From 2e1a98ff196c0047dd9d34c63716a8c8c795de84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 16 Feb 2026 12:30:15 -0800 Subject: [PATCH] Add useful view for note-not-found state in quoted notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog-Added: Added a view for quotes notes that could not be loaded, including actionable items Signed-off-by: Daniel D’Aquino --- damus/Features/Events/EventLoaderView.swift | 83 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/damus/Features/Events/EventLoaderView.swift b/damus/Features/Events/EventLoaderView.swift index 648dca18..990b3b1e 100644 --- a/damus/Features/Events/EventLoaderView.swift +++ b/damus/Features/Events/EventLoaderView.swift @@ -15,7 +15,8 @@ struct EventLoaderView: View { let event_id: NoteId let relayHints: [RelayURL] @State var event: NostrEvent? - @State var subscription_uuid: String = UUID().description + @State private var eventNotFound: Bool = false + @State private var isReloading: Bool = false let content: (NostrEvent) -> Content /// Creates an event loader view. @@ -34,6 +35,17 @@ struct EventLoaderView: View { _event = State(initialValue: event) } + /// Loads the event from nostrdb or the network using relay hints or the default relay pool. + /// + /// This method attempts to fetch the event via `nostrNetwork.reader.lookup`, which first checks + /// nostrdb for a cached copy, then queries the network if not found locally. Network queries use + /// either the specified relay hints (if provided) or the user's relay pool (if no hints are provided). + /// On success, sets `event` and clears `eventNotFound`. On failure, sets `eventNotFound` to true. + /// + /// Side effects: + /// - Updates `event` with the fetched event on success + /// - Updates `eventNotFound` flag based on the result + /// - Logs debug information when relay hints are used func load() async { let targetRelays = relayHints.isEmpty ? nil : relayHints #if DEBUG @@ -42,7 +54,14 @@ struct EventLoaderView: View { } #endif let lender = try? await damus_state.nostrNetwork.reader.lookup(noteId: self.event_id, to: targetRelays) - lender?.justUseACopy({ event = $0 }) + if let foundEvent = lender?.justGetACopy() { + event = foundEvent + eventNotFound = false + } + else { + // Handle nil case: event was not found + eventNotFound = true + } #if DEBUG if let targetRelays, !targetRelays.isEmpty { print("[relay-hints] EventLoaderView: Event \(event_id.hex().prefix(8))... loaded: \(event != nil)") @@ -50,10 +69,27 @@ struct EventLoaderView: View { #endif } + /// Retries loading the event and displays loading state during the operation. + /// + /// This method sets the `isReloading` flag to true, calls `load()`, and resets + /// the flag when complete. It is typically triggered by user action (e.g., "Try Again" button). + /// + /// Side effects: + /// - Updates `isReloading` to true during the operation + /// - Delegates to `load()`, which updates `event` and `eventNotFound` + /// - Resets `isReloading` to false after completion + func retry() async { + isReloading = true + await load() + isReloading = false + } + var body: some View { - VStack { + VStack { if let event { self.content(event) + } else if eventNotFound { + not_found } else { ProgressView().padding() } @@ -65,6 +101,47 @@ struct EventLoaderView: View { await self.load() } } + + var not_found: some View { + VStack(spacing: 0) { + LoadableNostrEventView.SomethingWrong( + imageSystemName: "questionmark.app", + heading: NSLocalizedString("Note not found", comment: "Heading for the event loader view in a not found error state."), + description: NSLocalizedString("This note may have been deleted, or it might not be available on the relays you're connected to.", comment: "Text for the event loader view when it is unable to find the note the user is looking for"), + advice: NSLocalizedString("Try checking your internet connection, expanding your relay list, or contacting the person who quoted this note.", comment: "Tips on what to do if a quoted note cannot be found.") + ) + + Button(action: { + Task { + await retry() + } + }) { + HStack { + if !isReloading { + Image(systemName: "arrow.clockwise") + Text("Try Again", comment: "Button label to retry loading a note that was not found") + } + else { + ProgressView() + Text("Retrying…", comment: "Button label for the retry-in-progress state when loading a note") + } + } + .font(.headline) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background(Color.secondary) + .cornerRadius(10) + } + .disabled(isReloading) + .opacity(isReloading ? 0.6 : 1.0) + .padding(.bottom, 20) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.secondary.opacity(0.3), lineWidth: 1) + ) + } }