SuggestedHashtagsView.swift (5497B)
1 // 2 // SuggestedHashtagsView.swift 3 // damus 4 // 5 // Created by Daniel D’Aquino on 2023-10-09. 6 // 7 8 import SwiftUI 9 10 // Currently we have a hardcoded list of possible hashtags that might be nice to suggest, 11 // and we suggest the top-N ones most active in the past day. 12 // This might be simple and effective until we find a more sophisticated way to let the user discover new hashtags 13 let DEFAULT_SUGGESTED_HASHTAGS: [String] = [ 14 "grownostr", "damus", "zapathon", "introductions", "plebchain", "bitcoin", "food", 15 "coffeechain", "nostr", "asknostr", "bounty", "freedom", "freedomtech", "foodstr", 16 "memestr", "memes", "music", "musicstr", "art", "artstr" 17 ] 18 19 struct SuggestedHashtagsView: View { 20 struct HashtagWithUserCount: Hashable { 21 var hashtag: String 22 var count: Int 23 } 24 25 let damus_state: DamusState 26 @StateObject var events: EventHolder 27 @SceneStorage("SuggestedHashtagsView.show_suggested_hashtags") var show_suggested_hashtags : Bool = true 28 var item_limit: Int? 29 let suggested_hashtags: [String] 30 var hashtags_with_count_to_display: [HashtagWithUserCount] { 31 get { 32 let all_items = self.suggested_hashtags 33 .map({ hashtag in 34 return HashtagWithUserCount( 35 hashtag: hashtag, 36 count: self.users_talking_about(hashtag: Hashtag(hashtag: hashtag)) 37 ) 38 }) 39 .sorted(by: { a, b in 40 a.count > b.count 41 }) 42 guard let item_limit else { 43 return all_items 44 } 45 return Array(all_items.prefix(item_limit)) 46 } 47 } 48 49 init(damus_state: DamusState, suggested_hashtags: [String]? = nil, max_items item_limit: Int? = nil, events: EventHolder) { 50 self.damus_state = damus_state 51 self.suggested_hashtags = suggested_hashtags ?? DEFAULT_SUGGESTED_HASHTAGS 52 self.item_limit = item_limit 53 _events = StateObject.init(wrappedValue: events) 54 } 55 56 var body: some View { 57 VStack { 58 HStack { 59 Image(systemName: "sparkles") 60 Text("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags") 61 Spacer() 62 Button(action: { 63 withAnimation(.easeOut(duration: 0.2)) { 64 show_suggested_hashtags.toggle() 65 } 66 }) { 67 if show_suggested_hashtags { 68 Image(systemName: "rectangle.compress.vertical") 69 .foregroundStyle(PinkGradient) 70 } else { 71 Image(systemName: "rectangle.expand.vertical") 72 .foregroundStyle(PinkGradient) 73 } 74 } 75 } 76 .foregroundColor(.secondary) 77 .padding(.vertical, 10) 78 79 if show_suggested_hashtags { 80 ForEach(hashtags_with_count_to_display, 81 id: \.self) { hashtag_with_count in 82 SuggestedHashtagView(damus_state: damus_state, hashtag: hashtag_with_count.hashtag, count: hashtag_with_count.count) 83 } 84 } 85 } 86 .padding(.horizontal) 87 } 88 89 private struct SuggestedHashtagView: View { // Purposefully private to SuggestedHashtagsView because it assumes the same 24h window 90 let damus_state: DamusState 91 let hashtag: String 92 let count: Int 93 94 init(damus_state: DamusState, hashtag: String, count: Int) { 95 self.damus_state = damus_state 96 self.hashtag = hashtag 97 self.count = count 98 } 99 100 var body: some View { 101 HStack { 102 SingleCharacterAvatar(character: "#") 103 104 VStack(alignment: .leading, spacing: 10) { 105 Text(verbatim: "#\(hashtag)") 106 .bold() 107 108 let pluralizedString = pluralizedString(key: "users_talking_about_it", count: self.count) 109 Text(pluralizedString) 110 .foregroundStyle(.secondary) 111 } 112 113 Spacer() 114 } 115 .onTapGesture { 116 let search_model = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag])) 117 damus_state.nav.push(route: Route.Search(search: search_model)) 118 } 119 } 120 } 121 122 func users_talking_about(hashtag: Hashtag) -> Int { 123 return self.events.all_events 124 .filter({ $0.referenced_hashtags.contains(hashtag)}) 125 .reduce(Set<Pubkey>([]), { authors, note in 126 return authors.union([note.pubkey]) 127 }) 128 .count 129 } 130 } 131 132 struct SuggestedHashtagsView_Previews: PreviewProvider { 133 static var previews: some View { 134 let time_window: TimeInterval = 24 * 60 * 60 // 1 day 135 let search_model = SearchModel( 136 state: test_damus_state, 137 search: NostrFilter.init( 138 since: UInt32(Date.now.timeIntervalSince1970 - time_window), 139 hashtag: ["nostr", "bitcoin", "zapathon"] 140 ) 141 ) 142 143 SuggestedHashtagsView( 144 damus_state: test_damus_state, 145 events: search_model.events 146 ) 147 } 148 } 149