SearchResultsView.swift (9510B)
1 // 2 // SearchResultsView.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-06-06. 6 // 7 8 import SwiftUI 9 10 struct MultiSearch { 11 let text: String 12 let hashtag: String 13 let profiles: [Pubkey] 14 } 15 16 enum Search: Identifiable { 17 case profiles([Pubkey]) 18 case hashtag(String) 19 case profile(Pubkey) 20 case note(NoteId) 21 case nip05(String) 22 case hex(Data) 23 case multi(MultiSearch) 24 case nevent(NEvent) 25 case naddr(NAddr) 26 case nprofile(NProfile) 27 28 var id: String { 29 switch self { 30 case .profiles: return "profiles" 31 case .hashtag: return "hashtag" 32 case .profile: return "profile" 33 case .note: return "note" 34 case .nip05: return "nip05" 35 case .hex: return "hex" 36 case .multi: return "multi" 37 case .nevent: return "nevent" 38 case .naddr: return "naddr" 39 case .nprofile: return "nprofile" 40 } 41 } 42 } 43 44 struct InnerSearchResults: View { 45 let damus_state: DamusState 46 let search: Search? 47 @Binding var results: [NostrEvent] 48 49 func ProfileSearchResult(pk: Pubkey) -> some View { 50 FollowUserView(target: .pubkey(pk), damus_state: damus_state) 51 } 52 53 func HashtagSearch(_ ht: String) -> some View { 54 let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht])) 55 return NavigationLink(value: Route.Search(search: search_model)) { 56 HStack { 57 Text("#\(ht)", comment: "Navigation link to search hashtag.") 58 } 59 .padding(.horizontal, 15) 60 .padding(.vertical, 5) 61 .background(DamusColors.neutral1) 62 .cornerRadius(20) 63 .overlay( 64 RoundedRectangle(cornerRadius: 20) 65 .stroke(DamusColors.neutral3, lineWidth: 1) 66 ) 67 } 68 } 69 70 func TextSearch(_ txt: String) -> some View { 71 return NavigationLink(value: Route.NDBSearch(results: $results)) { 72 HStack { 73 Text("Search word: \(txt)", comment: "Navigation link to search for a word.") 74 } 75 .padding(.horizontal, 15) 76 .padding(.vertical, 5) 77 .background(DamusColors.neutral1) 78 .cornerRadius(20) 79 .overlay( 80 RoundedRectangle(cornerRadius: 20) 81 .stroke(DamusColors.neutral3, lineWidth: 1) 82 ) 83 } 84 } 85 86 func ProfilesSearch(_ results: [Pubkey]) -> some View { 87 return LazyVStack { 88 ForEach(results, id: \.id) { pk in 89 ProfileSearchResult(pk: pk) 90 } 91 } 92 } 93 94 var body: some View { 95 Group { 96 switch search { 97 case .profiles(let results): 98 ProfilesSearch(results) 99 case .hashtag(let ht): 100 HashtagSearch(ht) 101 case .nip05(let addr): 102 SearchingEventView(state: damus_state, search_type: .nip05(addr)) 103 case .profile(let pubkey): 104 SearchingEventView(state: damus_state, search_type: .profile(pubkey)) 105 case .hex(let h): 106 VStack(spacing: 10) { 107 SearchingEventView(state: damus_state, search_type: .event(NoteId(h))) 108 SearchingEventView(state: damus_state, search_type: .profile(Pubkey(h))) 109 } 110 case .note(let nid): 111 SearchingEventView(state: damus_state, search_type: .event(nid)) 112 case .nevent(let nevent): 113 SearchingEventView(state: damus_state, search_type: .event(nevent.noteid)) 114 case .nprofile(let nprofile): 115 SearchingEventView(state: damus_state, search_type: .profile(nprofile.author)) 116 case .naddr(let naddr): 117 SearchingEventView(state: damus_state, search_type: .naddr(naddr)) 118 case .multi(let multi): 119 VStack(alignment: .leading) { 120 HStack(spacing: 20) { 121 HashtagSearch(multi.hashtag) 122 TextSearch(multi.text) 123 } 124 .padding(.bottom, 10) 125 126 ProfilesSearch(multi.profiles) 127 } 128 129 case .none: 130 Text("none", comment: "No search results.") 131 } 132 } 133 } 134 } 135 136 struct SearchResultsView: View { 137 let damus_state: DamusState 138 @Binding var search: String 139 @State var result: Search? = nil 140 @State var results: [NostrEvent] = [] 141 let debouncer: Debouncer = Debouncer(interval: 0.25) 142 143 func do_search(query: String) { 144 let limit = 128 145 var note_keys = damus_state.ndb.text_search(query: query, limit: limit, order: .newest_first) 146 var res = [NostrEvent]() 147 // TODO: fix duplicate results from search 148 var keyset = Set<NoteKey>() 149 150 // try reverse because newest first is a bit buggy on partial searches 151 if note_keys.count == 0 { 152 // don't touch existing results if there are no new ones 153 return 154 } 155 156 do { 157 guard let txn = NdbTxn(ndb: damus_state.ndb) else { return } 158 for note_key in note_keys { 159 guard let note = damus_state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else { 160 continue 161 } 162 163 if !keyset.contains(note_key) { 164 let owned_note = note.to_owned() 165 res.append(owned_note) 166 keyset.insert(note_key) 167 } 168 } 169 } 170 171 let res_ = res 172 173 Task { @MainActor [res_] in 174 results = res_ 175 } 176 } 177 178 var body: some View { 179 ScrollView { 180 InnerSearchResults(damus_state: damus_state, search: result, results: $results) 181 .padding() 182 } 183 .frame(maxHeight: .infinity) 184 .onAppear { 185 guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } 186 self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn) 187 } 188 .onChange(of: search) { new in 189 guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } 190 self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn) 191 } 192 .onChange(of: search) { query in 193 debouncer.debounce { 194 Task.detached { 195 do_search(query: query) 196 } 197 } 198 } 199 } 200 } 201 202 /* 203 struct SearchResultsView_Previews: PreviewProvider { 204 static var previews: some View { 205 SearchResultsView(damus_state: test_damus_state(), s) 206 } 207 } 208 */ 209 210 211 func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn<Y>) -> Search? { 212 guard new.count != 0 else { 213 return nil 214 } 215 216 let splitted = new.split(separator: "@") 217 218 if splitted.count == 2 { 219 return .nip05(new) 220 } 221 222 if new.first! == "#" { 223 return .hashtag(make_hashtagable(new)) 224 } 225 226 let searchQuery = remove_nostr_uri_prefix(new) 227 228 if let new = hex_decode_id(searchQuery) { 229 return .hex(new) 230 } 231 232 if searchQuery.starts(with: "npub") { 233 if let decoded = bech32_pubkey_decode(searchQuery) { 234 return .profile(decoded) 235 } 236 } 237 238 if searchQuery.starts(with: "note"), let decoded = try? bech32_decode(searchQuery) { 239 return .note(NoteId(decoded.data)) 240 } 241 242 if searchQuery.starts(with: "nevent"), case let .nevent(nevent) = Bech32Object.parse(searchQuery) { 243 return .nevent(nevent) 244 } 245 246 if searchQuery.starts(with: "nprofile"), case let .nprofile(nprofile) = Bech32Object.parse(searchQuery) { 247 return .nprofile(nprofile) 248 } 249 250 if searchQuery.starts(with: "naddr"), case let .naddr(naddr) = Bech32Object.parse(searchQuery) { 251 return .naddr(naddr) 252 } 253 254 let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn)) 255 return .multi(multisearch) 256 } 257 258 func make_hashtagable(_ str: String) -> String { 259 var new = str 260 guard str.utf8.count > 0 else { 261 return str 262 } 263 264 if new.hasPrefix("#") { 265 new = String(new.dropFirst()) 266 } 267 268 return String(new.filter{$0 != " "}) 269 } 270 271 func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn<Y>) -> [Pubkey] { 272 // Search by hex pubkey. 273 if let pubkey = hex_decode_pubkey(search), 274 profiles.lookup_key_by_pubkey(pubkey) != nil 275 { 276 return [pubkey] 277 } 278 279 // Search by npub pubkey. 280 if search.starts(with: "npub"), 281 let bech32_key = decode_bech32_key(search), 282 case Bech32Key.pub(let pk) = bech32_key, 283 profiles.lookup_key_by_pubkey(pk) != nil 284 { 285 return [pk] 286 } 287 288 return profiles.search(search, limit: 128, txn: txn).sorted { a, b in 289 let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0 290 let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0 291 292 if aFriendTypePriority > bFriendTypePriority { 293 // `a` should be sorted before `b` 294 return true 295 } else { 296 return false 297 } 298 } 299 } 300