damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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