From 62772615b6bf3515f727d36baa65080bf382b091 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Wed, 11 Sep 2024 20:34:24 -0600 Subject: [PATCH] ui: add ndb search to universe view This PR adds the NDB search functionality from the pull down search in the posting timeline to the universe view. Changelog-Added: Added NDB search functionality to the universe view Signed-off-by: ericholguin --- damus.xcodeproj/project.pbxproj | 10 ++- damus/Util/Router.swift | 5 ++ damus/Views/Search/NDBSearchView.swift | 51 +++++++++++++++ damus/Views/SearchResultsView.swift | 87 ++++++++++++++++++++++++-- 4 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 damus/Views/Search/NDBSearchView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 6443fbd6..4d652e8c 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -395,6 +395,9 @@ 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; + 5C0567582C8FBC560073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; }; + 5C0567592C8FBDE30073F23A /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; + 5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; 5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; }; 5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; }; @@ -928,7 +931,6 @@ D73E5F782C6A9A5C007EB227 /* NdbNote+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D798D2272B085CDA00234419 /* NdbNote+.swift */; }; D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; }; D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; }; - D73E5F7B2C6A9D0F007EB227 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D73E5F7C2C6A9D4F007EB227 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE827F7A08100C66700 /* ContentView.swift */; }; D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */; }; D73E5F812C6AA07A007EB227 /* HighlighterExtensionAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */; }; @@ -1832,6 +1834,7 @@ 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = ""; }; 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = ""; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; + 5C0567572C8FBC560073F23A /* NDBSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NDBSearchView.swift; sourceTree = ""; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = ""; }; 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = ""; }; 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = ""; }; @@ -2962,6 +2965,7 @@ children = ( 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */, 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */, + 5C0567572C8FBC560073F23A /* NDBSearchView.swift */, ); path = Search; sourceTree = ""; @@ -4021,6 +4025,7 @@ 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */, 4CDD1AE02A6B305F001CD4DF /* NdbTagElem.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, + 5C0567582C8FBC560073F23A /* NDBSearchView.swift in Sources */, D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */, 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, 4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */, @@ -4338,6 +4343,7 @@ D73E5E7A2C6A97F4007EB227 /* EventCache.swift in Sources */, D73E5E7B2C6A97F4007EB227 /* DebouncedOnChange.swift in Sources */, D73E5E7C2C6A97F4007EB227 /* ReplyCounter.swift in Sources */, + 5C0567592C8FBDE30073F23A /* Router.swift in Sources */, D73E5E7D2C6A97F4007EB227 /* CompatibleAttribute.swift in Sources */, D73E5E7E2C6A97F4007EB227 /* Hashtags.swift in Sources */, D73E5E7F2C6A97F4007EB227 /* LocalNotification.swift in Sources */, @@ -4622,7 +4628,6 @@ D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */, D703D7A92C670E5A00A400EA /* refmap.c in Sources */, D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */, - D73E5F7B2C6A9D0F007EB227 /* Router.swift in Sources */, D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */, D703D79D2C670E0700A400EA /* node_id.c in Sources */, D703D79B2C670E0000A400EA /* bech32_util.c in Sources */, @@ -4633,6 +4638,7 @@ D703D76B2C670B3100A400EA /* Referenced.swift in Sources */, D703D7952C670DE600A400EA /* hash_u5.c in Sources */, D703D7582C670A6000A400EA /* Id.swift in Sources */, + 5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */, D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */, D703D7A02C670E1500A400EA /* take.c in Sources */, D703D7692C670B2600A400EA /* Block.swift in Sources */, diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index 681138f9..8e45d100 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -37,6 +37,7 @@ enum Route: Hashable { case Reactions(reactions: EventsModel) case Zaps(target: ZapTarget) case Search(search: SearchModel) + case NDBSearch(results: Binding<[NostrEvent]>) case EULA case Login case CreateAccount @@ -105,6 +106,8 @@ enum Route: Hashable { ZapsView(state: damusState, target: target) case .Search(let search): SearchView(appstate: damusState, search: search) + case .NDBSearch(let results): + NDBSearchView(damus_state: damusState, results: results) case .EULA: EULAView(nav: navigationCoordinator) case .Login: @@ -200,6 +203,8 @@ enum Route: Hashable { case .Search(let search): hasher.combine("search") hasher.combine(search.search) + case .NDBSearch(let results): + hasher.combine("results") case .EULA: hasher.combine("eula") case .Login: diff --git a/damus/Views/Search/NDBSearchView.swift b/damus/Views/Search/NDBSearchView.swift new file mode 100644 index 00000000..974c2f5e --- /dev/null +++ b/damus/Views/Search/NDBSearchView.swift @@ -0,0 +1,51 @@ +// +// NDBSearchView.swift +// damus +// +// Created by eric on 9/9/24. +// + +import SwiftUI + +struct NDBSearchView: View { + + let damus_state: DamusState + @Binding var results: [NostrEvent] + + var body: some View { + ScrollView { + if results.count > 0 { + HStack { + Spacer() + Image("search") + Text("Top hits", comment: "A label indicating that the notes being displayed below it are all top note search results") + Spacer() + } + .padding() + .foregroundColor(.secondary) + + ForEach(results, id: \.self) { note in + EventView(damus: damus_state, event: note) + .onTapGesture { + let event = note.get_inner_event(cache: damus_state.events) ?? note + let thread = ThreadModel(event: event, damus_state: damus_state) + damus_state.nav.push(route: Route.Thread(thread: thread)) + } + .padding(.horizontal) + + ThiccDivider() + } + + } else if results.count == 0 { + HStack { + Spacer() + Image("search") + Text("No results", comment: "A label indicating that note search resulted in no results") + Spacer() + } + .padding() + .foregroundColor(.secondary) + } + } + } +} diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift index 4af89f56..b014a530 100644 --- a/damus/Views/SearchResultsView.swift +++ b/damus/Views/SearchResultsView.swift @@ -8,6 +8,7 @@ import SwiftUI struct MultiSearch { + let text: String let hashtag: String let profiles: [Pubkey] } @@ -43,6 +44,7 @@ enum Search: Identifiable { struct InnerSearchResults: View { let damus_state: DamusState let search: Search? + @Binding var results: [NostrEvent] func ProfileSearchResult(pk: Pubkey) -> some View { FollowUserView(target: .pubkey(pk), damus_state: damus_state) @@ -51,7 +53,35 @@ struct InnerSearchResults: View { func HashtagSearch(_ ht: String) -> some View { let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht])) return NavigationLink(value: Route.Search(search: search_model)) { - Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.") + HStack { + Image("search") + Text("#\(ht)", comment: "Navigation link to search hashtag.") + } + .padding(.horizontal, 15) + .padding(.vertical, 5) + .background(DamusColors.neutral1) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + } + } + + func TextSearch(_ txt: String) -> some View { + return NavigationLink(value: Route.NDBSearch(results: $results)) { + HStack { + Image("search") + Text("Notes", comment: "Navigation link to search text.") + } + .padding(.horizontal, 15) + .padding(.vertical, 5) + .background(DamusColors.neutral1) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) } } @@ -88,8 +118,11 @@ struct InnerSearchResults: View { case .naddr(let naddr): SearchingEventView(state: damus_state, search_type: .naddr(naddr)) case .multi(let multi): - VStack { - HashtagSearch(multi.hashtag) + VStack(alignment: .leading) { + HStack { + HashtagSearch(multi.hashtag) + TextSearch(multi.text) + } ProfilesSearch(multi.profiles) } @@ -104,10 +137,47 @@ struct SearchResultsView: View { let damus_state: DamusState @Binding var search: String @State var result: Search? = nil + @State var results: [NostrEvent] = [] + let debouncer: Debouncer = Debouncer(interval: 0.25) + + func do_search(query: String) { + let limit = 16 + var note_keys = damus_state.ndb.text_search(query: query, limit: limit, order: .newest_first) + var res = [NostrEvent]() + // TODO: fix duplicate results from search + var keyset = Set() + + // try reverse because newest first is a bit buggy on partial searches + if note_keys.count == 0 { + // don't touch existing results if there are no new ones + return + } + + do { + guard let txn = NdbTxn(ndb: damus_state.ndb) else { return } + for note_key in note_keys { + guard let note = damus_state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else { + continue + } + + if !keyset.contains(note_key) { + let owned_note = note.to_owned() + res.append(owned_note) + keyset.insert(note_key) + } + } + } + + let res_ = res + + Task { @MainActor [res_] in + results = res_ + } + } var body: some View { ScrollView { - InnerSearchResults(damus_state: damus_state, search: result) + InnerSearchResults(damus_state: damus_state, search: result, results: $results) .padding() } .frame(maxHeight: .infinity) @@ -119,6 +189,13 @@ struct SearchResultsView: View { guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn) } + .onChange(of: search) { query in + debouncer.debounce { + Task.detached { + do_search(query: query) + } + } + } } } @@ -174,7 +251,7 @@ func search_for_string(profiles: Profiles, contacts: Contacts, search new: St return .naddr(naddr) } - let multisearch = MultiSearch(hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn)) + let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn)) return .multi(multisearch) }