1
0
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:
Daniel D’Aquino 2023-10-20 18:16:13 +00:00 committed by William Casarin
parent bf43842590
commit 7ae66b8490
4 changed files with 153 additions and 1 deletions

View File

@ -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 */,

View File

@ -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) {

View File

@ -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 {

View File

@ -0,0 +1,135 @@
//
// SuggestedHashtagsView.swift
// damus
//
// Created by Daniel DAquino 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
)
}
}