mirror of
git://jb55.com/damus
synced 2024-09-30 00:40:45 +00:00
ui: Add suggested hashtags to universe view
This commit adds a suggested hashtag section to the universe view tab. The method for suggesting hashtags is currently simple: 1. It contains a list of many possible hashtags that we could recommend 2. It calculates how many users have been talking about it in the events fetched by the Universe tab 3. It selects the Top-N most mentioned suggested hashtags in the Universe tab This has the following properties: 1. It has some spam resistance as it ranks by unique users mentioning the tag (instead of events) 2. It is a simple way to curate good hashtags 3. It shows the ones with the most amount of people talking about it among the notes fecthed in the Universe view Testing ------- PASS Device: iPhone 14 Pro simulator iOS: 17.0 Damus: This commit Coverage: 1. Suggested hashtags are displayed 2. Layout looks similar to Figma 3. User count goes up (does not stay at zero) 4. Clicking on a suggested hashtag takes you to that hashtag view 5. Only the top 5 hashtags are displayed Notes: The counts seem low, probably because there are not enough notes loaded in Universe View Changelog-Added: Add suggested hashtags to universe view Closes: https://github.com/damus-io/damus/issues/1569 Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
parent
bf43842590
commit
7ae66b8490
@ -435,6 +435,7 @@
|
||||
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; };
|
||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
||||
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
|
||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
||||
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
|
||||
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
||||
@ -1132,6 +1133,7 @@
|
||||
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
|
||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
||||
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
|
||||
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
|
||||
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
|
||||
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
|
||||
@ -1719,6 +1721,7 @@
|
||||
50DA11252A16A23F00236234 /* Launch.storyboard */,
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
|
||||
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -2818,6 +2821,7 @@
|
||||
4CC14FF52A740BB7007AEB17 /* NoteId.swift in Sources */,
|
||||
4C19AE512A5CEF7C00C90DB7 /* NostrScript.swift in Sources */,
|
||||
4C32B95E2A9AD44700DC3548 /* FlatBufferObject.swift in Sources */,
|
||||
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */,
|
||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */,
|
||||
|
@ -17,7 +17,7 @@ class SearchHomeModel: ObservableObject {
|
||||
let damus_state: DamusState
|
||||
let base_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 250
|
||||
let limit: UInt32 = 500
|
||||
//let multiple_events_per_pubkey: Bool = false
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
|
@ -74,6 +74,19 @@ struct SearchHomeView: View {
|
||||
}
|
||||
|
||||
return preferredLanguages.contains(note_lang)
|
||||
},
|
||||
content: {
|
||||
AnyView(VStack {
|
||||
SuggestedHashtagsView(damus_state: damus_state, max_items: 5, events: model.events)
|
||||
HStack {
|
||||
Image(systemName: "bubble.fill")
|
||||
Text(NSLocalizedString("All recent notes", comment: "A label indicating that the notes being displayed below it are all recent notes"))
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal)
|
||||
})
|
||||
}
|
||||
)
|
||||
.refreshable {
|
||||
|
135
damus/Views/SuggestedHashtagsView.swift
Normal file
135
damus/Views/SuggestedHashtagsView.swift
Normal file
@ -0,0 +1,135 @@
|
||||
//
|
||||
// SuggestedHashtagsView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-10-09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Currently we have a hardcoded list of possible hashtags that might be nice to suggest,
|
||||
// and we suggest the top-N ones most active in the past day.
|
||||
// This might be simple and effective until we find a more sophisticated way to let the user discover new hashtags
|
||||
let DEFAULT_SUGGESTED_HASHTAGS: [String] = [
|
||||
"grownostr", "damus", "zapathon", "introductions", "plebchain", "bitcoin", "food",
|
||||
"coffeechain", "nostr", "asknostr", "bounty", "freedom", "freedomtech", "foodstr",
|
||||
"memestr", "memes", "music", "musicstr", "art", "artstr"
|
||||
]
|
||||
|
||||
struct SuggestedHashtagsView: View {
|
||||
struct HashtagWithUserCount: Hashable {
|
||||
var hashtag: String
|
||||
var count: Int
|
||||
}
|
||||
|
||||
let damus_state: DamusState
|
||||
@StateObject var events: EventHolder
|
||||
var item_limit: Int?
|
||||
let suggested_hashtags: [String]
|
||||
var hashtags_with_count_to_display: [HashtagWithUserCount] {
|
||||
get {
|
||||
let all_items = self.suggested_hashtags
|
||||
.map({ hashtag in
|
||||
return HashtagWithUserCount(
|
||||
hashtag: hashtag,
|
||||
count: self.users_talking_about(hashtag: Hashtag(hashtag: hashtag))
|
||||
)
|
||||
})
|
||||
.sorted(by: { a, b in
|
||||
a.count > b.count
|
||||
})
|
||||
guard let item_limit else {
|
||||
return all_items
|
||||
}
|
||||
return Array(all_items.prefix(item_limit))
|
||||
}
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, suggested_hashtags: [String]? = nil, max_items item_limit: Int? = nil, events: EventHolder) {
|
||||
self.damus_state = damus_state
|
||||
self.suggested_hashtags = suggested_hashtags ?? DEFAULT_SUGGESTED_HASHTAGS
|
||||
self.item_limit = item_limit
|
||||
_events = StateObject.init(wrappedValue: events)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "sparkles")
|
||||
Text(NSLocalizedString("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags"))
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
ForEach(hashtags_with_count_to_display,
|
||||
id: \.self) { hashtag_with_count in
|
||||
SuggestedHashtagView(damus_state: damus_state, hashtag: hashtag_with_count.hashtag, count: hashtag_with_count.count)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private struct SuggestedHashtagView: View { // Purposefully private to SuggestedHashtagsView because it assumes the same 24h window
|
||||
let damus_state: DamusState
|
||||
let hashtag: String
|
||||
let count: Int
|
||||
|
||||
init(damus_state: DamusState, hashtag: String, count: Int) {
|
||||
self.damus_state = damus_state
|
||||
self.hashtag = hashtag
|
||||
self.count = count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
SingleCharacterAvatar(character: "#")
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("#\(hashtag)")
|
||||
.bold()
|
||||
|
||||
Text(self.count != 1 ? String(
|
||||
format: NSLocalizedString("%d users talking about it", comment: "A label indicating how many users have been talking about a hashtag"),
|
||||
self.count
|
||||
) : NSLocalizedString("1 user talking about it", comment: "A label indicating 1 user has been talking about a hashtag"))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.onTapGesture {
|
||||
let search_model = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag]))
|
||||
damus_state.nav.push(route: Route.Search(search: search_model))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func users_talking_about(hashtag: Hashtag) -> Int {
|
||||
return self.events.all_events
|
||||
.filter({ $0.referenced_hashtags.contains(hashtag)})
|
||||
.reduce(Set<Pubkey>([]), { authors, note in
|
||||
return authors.union([note.pubkey])
|
||||
})
|
||||
.count
|
||||
}
|
||||
}
|
||||
|
||||
struct SuggestedHashtagsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let time_window: TimeInterval = 24 * 60 * 60 // 1 day
|
||||
let search_model = SearchModel(
|
||||
state: test_damus_state,
|
||||
search: NostrFilter.init(
|
||||
since: UInt32(Date.now.timeIntervalSince1970 - time_window),
|
||||
hashtag: ["nostr", "bitcoin", "zapathon"]
|
||||
)
|
||||
)
|
||||
|
||||
SuggestedHashtagsView(
|
||||
damus_state: test_damus_state,
|
||||
events: search_model.events
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user