commit 480921db20b1dc4ada8a43c4088f54e4fd173bb8
parent f0de8721c7b40c4273ed3ba86732462e7d3bb3e4
Author: Joel Klabo <joelklabo@gmail.com>
Date: Thu, 20 Jul 2023 12:45:10 -0700
Suggested Users to Follow
ui: Add Suggested Users Views and Helpers
ui: Add Logic to Launch Suggested User Screen
Changelog-Added: Suggested Users to Follow
Diffstat:
8 files changed, 487 insertions(+), 1 deletion(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -348,6 +348,12 @@
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
+ F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* SuggestedUsersView.swift */; };
+ F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; };
+ F71694EE2A6624F9001F4053 /* suggested_users.json in Resources */ = {isa = PBXBuildFile; fileRef = F71694ED2A6624F9001F4053 /* suggested_users.json */; };
+ F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F12A67314D001F4053 /* SuggestedUserView.swift */; };
+ F71694F42A6732B7001F4053 /* GradientFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F32A6732B7001F4053 /* GradientFollowButton.swift */; };
+ F71694F82A6983AF001F4053 /* GrayGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F72A6983AF001F4053 /* GrayGradient.swift */; };
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
@@ -855,6 +861,12 @@
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
+ F71694E92A662232001F4053 /* SuggestedUsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersView.swift; sourceTree = "<group>"; };
+ F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersViewModel.swift; sourceTree = "<group>"; };
+ F71694ED2A6624F9001F4053 /* suggested_users.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested_users.json; sourceTree = "<group>"; };
+ F71694F12A67314D001F4053 /* SuggestedUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUserView.swift; sourceTree = "<group>"; };
+ F71694F32A6732B7001F4053 /* GradientFollowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientFollowButton.swift; sourceTree = "<group>"; };
+ F71694F72A6983AF001F4053 /* GrayGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayGradient.swift; sourceTree = "<group>"; };
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
@@ -1146,6 +1158,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
+ F71694E82A66221E001F4053 /* Onboarding */,
4C190F232A547D1700027FD5 /* NostrScript */,
4C7D09692A0AEA0400943473 /* CodeScanner */,
4C7D095A2A098C5C00943473 /* Wallet */,
@@ -1263,6 +1276,7 @@
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */,
+ F71694F72A6983AF001F4053 /* GrayGradient.swift */,
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
4C687C202A5F7ED00092C550 /* DamusBackground.swift */,
);
@@ -1325,6 +1339,7 @@
isa = PBXGroup;
children = (
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
+ F71694F32A6732B7001F4053 /* GradientFollowButton.swift */,
4C7D09652A0AE62100943473 /* AlbyButton.swift */,
);
path = Buttons;
@@ -1707,6 +1722,17 @@
path = Extensions;
sourceTree = "<group>";
};
+ F71694E82A66221E001F4053 /* Onboarding */ = {
+ isa = PBXGroup;
+ children = (
+ F71694E92A662232001F4053 /* SuggestedUsersView.swift */,
+ F71694F12A67314D001F4053 /* SuggestedUserView.swift */,
+ F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */,
+ F71694ED2A6624F9001F4053 /* suggested_users.json */,
+ );
+ path = Onboarding;
+ sourceTree = "<group>";
+ };
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -1875,6 +1901,7 @@
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
4C198DF129F88C6B004C165C /* License.txt in Resources */,
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
+ F71694EE2A6624F9001F4053 /* suggested_users.json in Resources */,
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1984,6 +2011,7 @@
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
4C4DD3DB2A6CA7E8005B4E85 /* ContentParsing.swift in Sources */,
+ F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */,
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
@@ -2073,6 +2101,7 @@
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
+ F71694F82A6983AF001F4053 /* GrayGradient.swift in Sources */,
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
@@ -2112,6 +2141,7 @@
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
+ F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */,
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
@@ -2145,6 +2175,7 @@
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
+ F71694F42A6732B7001F4053 /* GradientFollowButton.swift in Sources */,
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
@@ -2168,6 +2199,7 @@
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
+ F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
diff --git a/damus/Components/Gradients/GrayGradient.swift b/damus/Components/Gradients/GrayGradient.swift
@@ -0,0 +1,26 @@
+//
+// GrayGradient.swift
+// damus
+//
+// Created by klabo on 7/20/23.
+//
+
+import SwiftUI
+
+let GrayGradient = LinearGradient(gradient:
+ Gradient(colors: [Color(#colorLiteral(red: 0.9764705882, green: 0.9803921569, blue: 0.9803921569, alpha: 1))]),
+ startPoint: .leading,
+ endPoint: .trailing)
+
+struct GrayGradientView: View {
+ var body: some View {
+ GrayGradient
+ .edgesIgnoringSafeArea([.top, .bottom])
+ }
+}
+
+struct GrayGradient_Previews: PreviewProvider {
+ static var previews: some View {
+ GrayGradientView()
+ }
+}
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -30,6 +30,7 @@ enum Sheets: Identifiable {
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter
+ case suggestedUsers
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
@@ -47,6 +48,7 @@ enum Sheets: Identifiable {
case .zap(let sheet): return "zap-" + sheet.target.id
case .select_wallet: return "select-wallet"
case .filter: return "filter"
+ case .suggestedUsers: return "suggested-users"
}
}
}
@@ -89,7 +91,7 @@ struct ContentView: View {
@State private var isSideBarOpened = false
var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
-
+ @AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme
@@ -302,6 +304,10 @@ struct ContentView: View {
self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
setup_notifications()
+ if !hasSeenSuggestedUsers {
+ active_sheet = .suggestedUsers
+ hasSeenSuggestedUsers = true
+ }
}
.sheet(item: $active_sheet) { item in
switch item {
@@ -324,6 +330,8 @@ struct ContentView: View {
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
+ case .suggestedUsers:
+ SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!))
}
}
.onOpenURL { url in
diff --git a/damus/Views/Buttons/GradientFollowButton.swift b/damus/Views/Buttons/GradientFollowButton.swift
@@ -0,0 +1,83 @@
+//
+// GradientFollowButton.swift
+// damus
+//
+// Created by klabo on 7/18/23.
+//
+
+import SwiftUI
+
+struct GradientFollowButton: View {
+
+ let target: FollowTarget
+ let follows_you: Bool
+
+ @State var follow_state: FollowState
+
+ private let grayTextColor = Color(#colorLiteral(red: 0.1450980392, green: 0.1607843137, blue: 0.1764705882, alpha: 1))
+ private let grayBorder = Color(#colorLiteral(red: 0.8666666667, green: 0.8823529412, blue: 0.8901960784, alpha: 1))
+
+ var body: some View {
+
+ Button(action: {
+ follow_state = perform_follow_btn_action(follow_state, target: target)
+ }) {
+ Text("\(follow_btn_txt(follow_state, follows_you: follows_you))")
+ .foregroundColor(follow_state == .unfollows ? .white : grayTextColor)
+ .font(.callout)
+ .fontWeight(.medium)
+ .padding([.top, .bottom], 10)
+ .padding([.leading, .trailing], 12)
+ .background(follow_state == .unfollows ? PinkGradient : GrayGradient)
+ .cornerRadius(12)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(grayBorder, lineWidth: follow_state == .unfollows ? 0 : 1)
+ )
+ }
+ .onReceive(handle_notify(.followed)) { notif in
+ let pk = notif.object as? ReferencedId
+ if pk?.ref_id != target.pubkey {
+ return
+ }
+
+ self.follow_state = .follows
+ }
+ .onReceive(handle_notify(.unfollowed)) { notif in
+ let pk = notif.object as? ReferencedId
+ if pk?.ref_id != target.pubkey {
+ return
+ }
+
+ self.follow_state = .unfollows
+ }
+ }
+}
+
+struct GradientFollowButtonPreviews: View {
+ let target: FollowTarget = .pubkey("")
+ var body: some View {
+ VStack {
+ Text(verbatim: "Unfollows")
+ GradientFollowButton(target: target, follows_you: false, follow_state: .unfollows)
+
+ Text(verbatim: "Following")
+ GradientFollowButton(target: target, follows_you: false, follow_state: .following)
+
+ Text(verbatim: "Follows")
+ GradientFollowButton(target: target, follows_you: false, follow_state: .follows)
+
+ Text(verbatim: "Follows")
+ GradientFollowButton(target: target, follows_you: true, follow_state: .follows)
+
+ Text(verbatim: "Unfollowing")
+ GradientFollowButton(target: target, follows_you: false, follow_state: .unfollowing)
+ }
+ }
+}
+
+struct GradientButton_Previews: PreviewProvider {
+ static var previews: some View {
+ GradientFollowButtonPreviews()
+ }
+}
diff --git a/damus/Views/Onboarding/SuggestedUserView.swift b/damus/Views/Onboarding/SuggestedUserView.swift
@@ -0,0 +1,72 @@
+//
+// SuggestedUserView.swift
+// damus
+//
+// Created by klabo on 7/18/23.
+//
+
+import SwiftUI
+
+struct SuggestedUser: Codable {
+ let pubkey: String
+ let name: String
+ let about: String
+ let pfp: URL
+ let profile: Profile
+
+ init?(profile: Profile, pubkey: String) {
+
+ guard let name = profile.name,
+ let about = profile.about,
+ let picture = profile.picture,
+ let pfpURL = URL(string: picture) else {
+ return nil
+ }
+
+ self.pubkey = pubkey
+ self.name = name
+ self.about = about
+ self.pfp = pfpURL
+ self.profile = profile
+ }
+}
+
+struct SuggestedUserView: View {
+
+ let user: SuggestedUser
+ let damus_state: DamusState
+
+ var body: some View {
+ HStack {
+ let target = FollowTarget.pubkey(user.pubkey)
+ InnerProfilePicView(url: user.pfp,
+ fallbackUrl: nil,
+ pubkey: target.pubkey,
+ size: 50,
+ highlight: .none,
+ disable_animation: false)
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ ProfileName(pubkey: user.pubkey, profile: user.profile, damus: damus_state)
+ }
+ Text(user.about)
+ .lineLimit(3)
+ .foregroundColor(.gray)
+ .font(.caption)
+ }
+ Spacer()
+ GradientFollowButton(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
+ }
+ }
+}
+
+struct SuggestedUserView_Previews: PreviewProvider {
+ static var previews: some View {
+ let profile = Profile(name: "klabo", about: "A person who likes nostr a lot and I like to tell people about myself in very long-winded ways that push the limits of UI and almost break things", picture: "https://primal.b-cdn.net/media-cache?s=m&a=1&u=https%3A%2F%2Fpbs.twimg.com%2Fprofile_images%2F1599994711430742017%2F33zLk9Wi_400x400.jpg")
+
+ let user = SuggestedUser(profile: profile, pubkey: "abcd")!
+ List {
+ SuggestedUserView(user: user, damus_state: test_damus_state())
+ }
+ }
+}
diff --git a/damus/Views/Onboarding/SuggestedUsersView.swift b/damus/Views/Onboarding/SuggestedUsersView.swift
@@ -0,0 +1,77 @@
+//
+// SuggestedUsersView.swift
+// damus
+//
+// Created by klabo on 7/17/23.
+//
+
+import SwiftUI
+
+struct SuggestedUsersView: View {
+
+ @StateObject var model: SuggestedUsersViewModel
+
+ @Environment(\.presentationMode) private var presentationMode
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ List {
+ ForEach(model.groups) { group in
+ Section {
+ ForEach(group.users, id: \.self) { pk in
+ if let user = model.suggestedUser(pubkey: pk) {
+ SuggestedUserView(user: user, damus_state: model.damus_state)
+ }
+ }
+ } header: {
+ SuggestedUsersSectionHeader(group: group, model: model)
+ }
+ }
+ }
+ .listStyle(.plain)
+
+ Spacer()
+
+ Button(action: {
+ presentationMode.wrappedValue.dismiss()
+ }) {
+ Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app"))
+ .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
+ }
+ .buttonStyle(GradientButtonStyle())
+ .padding([.leading, .trailing], 24)
+ .padding(.bottom, 16)
+ }
+ .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow"))
+ .navigationBarTitleDisplayMode(.inline)
+ .navigationBarItems(trailing: Button(action: {
+ presentationMode.wrappedValue.dismiss()
+ }, label: {
+ Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen"))
+ .font(.subheadline.weight(.semibold))
+ }))
+ }
+ }
+}
+
+struct SuggestedUsersSectionHeader: View {
+ let group: SuggestedUserGroup
+ let model: SuggestedUsersViewModel
+ var body: some View {
+ HStack {
+ Text(group.title.uppercased())
+ Spacer()
+ Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) {
+ model.follow(pubkeys: group.users)
+ }
+ .font(.subheadline.weight(.semibold))
+ }
+ }
+}
+
+struct SuggestedUsersView_Previews: PreviewProvider {
+ static var previews: some View {
+ SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: test_damus_state()))
+ }
+}
diff --git a/damus/Views/Onboarding/SuggestedUsersViewModel.swift b/damus/Views/Onboarding/SuggestedUsersViewModel.swift
@@ -0,0 +1,108 @@
+//
+// SuggestedUsersViewModel.swift
+// damus
+//
+// Created by klabo on 7/17/23.
+//
+
+import Foundation
+import Combine
+
+struct SuggestedUserGroup: Identifiable, Codable {
+ let id = UUID()
+ let title: String
+ let users: [String]
+
+ enum CodingKeys: String, CodingKey {
+ case title, users
+ }
+}
+
+
+class SuggestedUsersViewModel: ObservableObject {
+
+ public let damus_state: DamusState
+
+ @Published var groups: [SuggestedUserGroup] = []
+
+ private let sub_id = UUID().uuidString
+
+ init(damus_state: DamusState) {
+ self.damus_state = damus_state
+ loadSuggestedUserGroups()
+ let pubkeys = getPubkeys(groups: groups)
+ subscribeToSuggestedProfiles(pubkeys: pubkeys)
+ }
+
+ func suggestedUser(pubkey: String) -> SuggestedUser? {
+ if let profile = damus_state.profiles.lookup(id: pubkey),
+ let user = SuggestedUser(profile: profile, pubkey: pubkey) {
+ return user
+ }
+ return nil
+ }
+
+ func follow(pubkeys: [String]) {
+ for pubkey in pubkeys {
+ notify(.follow, FollowTarget.pubkey(pubkey))
+ }
+ }
+
+ private func loadSuggestedUserGroups() {
+ guard let url = Bundle.main.url(forResource: "suggested_users", withExtension: "json") else {
+ return
+ }
+
+ guard let data = try? Data(contentsOf: url) else {
+ return
+ }
+
+ let decoder = JSONDecoder()
+ do {
+ let groups = try decoder.decode([SuggestedUserGroup].self, from: data)
+ self.groups = groups
+ } catch {
+ print(error.localizedDescription.localizedLowercase)
+ }
+ }
+
+ private func getPubkeys(groups: [SuggestedUserGroup]) -> [String] {
+ var pubkeys: [String] = []
+ for group in groups {
+ pubkeys.append(contentsOf: group.users)
+ }
+ return pubkeys
+ }
+
+ private func subscribeToSuggestedProfiles(pubkeys: [String]) {
+ let filter = NostrFilter(kinds: [.metadata],
+ authors: pubkeys)
+ damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
+ }
+
+ func handle_event(relay_id: String, ev: NostrConnectionEvent) {
+ guard case .nostr_event(let nev) = ev else {
+ return
+ }
+
+ switch nev {
+ case .event(let sub_id, let ev):
+ guard sub_id == self.sub_id else {
+ return
+ }
+
+ if ev.known_kind == .metadata {
+ process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
+ }
+
+ case .notice(let msg):
+ print("suggested user profiles notice: \(msg)")
+
+ case .eose:
+ self.objectWillChange.send()
+
+ case .ok:
+ break
+ }
+ }
+}
diff --git a/damus/Views/Onboarding/suggested_users.json b/damus/Views/Onboarding/suggested_users.json
@@ -0,0 +1,80 @@
+[
+ {
+ "title": "nostr",
+ "users": [
+ "ba2f394833658475e91680b898f9be0f1d850166c6a839dbe084d0266ad6e20a",
+ "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
+ "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
+ "b708f7392f588406212c3882e7b3bc0d9b08d62f95fa170d099127ece2770e5e"
+ ]
+ },
+ {
+ "title": "permaculture & livestock & gardening",
+ "users": [
+ "4b1804c90d59dff195ee0e8f692b98a7c762bf1793b3e126c546d730dcb04477",
+ "2c24e1af571fb4ccfeca3981649c1b09c695cd83b129709eb3b41c7ad2854899",
+ "296842eaaed9be5ae0668da09fe48aac0521c4af859ad547d93145e5ac34c17e",
+ "2c24e1af571fb4ccfeca3981649c1b09c695cd83b129709eb3b41c7ad2854899"
+ ]
+ },
+ {
+ "title": "music",
+ "users": [
+ "23708a76e7090cb108d33e8801fd36262c475e5499b23eb87eee4a31f4f0144e",
+ "ca9d68eb25620fc755e1b8c76b5f155f4c7e96d99c532c109a8b36d208bdce55"
+ ]
+ },
+ {
+ "title": "books",
+ "users": [
+ "2652f3af10de63bc10a2628871a3fce62e08655e4fcf90a58be16f246bb65da3",
+ "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"
+ ]
+ },
+ {
+ "title": "art & photography",
+ "users": [
+ "f96c3d76497074c4c83a7b3823380e77dc73d5a9494fd2e053e4a1453e17824b",
+ "11b2d93b26d7e56fb57f0afce0d33bfa7fb35b913e4c0aeb7706464befb9ca97",
+ "f4db5270bd991b17bea1e6d035f45dee392919c29474bbac10342d223c74e0d0",
+ "af146f51634a5fb0abc592fbc2bed42cf740700990344a766a8999fe55eed1c6",
+ "8816e8938fe60a3a925433c77410f39500fe1320787acb2165f9345e70464592",
+ "8d0638d3e8d9b23e337fb4017bb7d5a97def40bdc258e8121ee7d51e623ca065",
+ "ece8ed2111f73b92251e74a3da22dee1d6c6d9b497040acc68a37ac6bbf5a105",
+ "64bfa9abffe5b18d0731eed57b38173adc2ba89bf87c168da90517f021e722b5",
+ "546bffcb9317edf919828de272dd8eba11f2aadc07969cba6c9bb894c07712ef",
+ "20998a8d433181cc5e778903db528d162f5579918a3437be40fdba0130b3469c",
+ "37c962bfb34d32e667d5075f7d55f3a7ca9bcddbafeff59a4003c276ad147352",
+ "387fa328198e67a400caf00947cc91e0c166d24d71d8b4b78a6c3ef91ff4d058",
+ "87deef9034c52d87ae327af3a76358e58e7ee2e03d80c738ee1a2be399c23e79"
+ ]
+ },
+ {
+ "title": "ai art",
+ "users": [
+ "431fa2f340f0adf8963d6d7c6e2c20d913278c691fe609fd3857db13d8f39feb",
+ "9936a53def39d712f886ac7e2ed509ce223b534834dd29d95caba9f6bc01ef35",
+ "1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b",
+ "693c2832de939b4af8ccd842b17f05df2edd551e59989d3c4ef9a44957b2f1fb",
+ "55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185"
+ ]
+ },
+ {
+ "title": "parenting",
+ "users": [
+ "c7c8f645fd45b09055fb6c26d148737ad7ed12ddecde0d4c877b88f8d4196865",
+ "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
+ "e451c4e82e272ef912a04f490373fce9054a044b11a2aa847823b3686c8eba77",
+ "261c7e18545aee4eb55e8052297fd93bd886c2f96b10d599cfdad4f3477b87ab",
+ "22a193fb86c793d7f73348126a5fe55ab8a4c975805ebc2ff81bf290f7805f7e"
+ ]
+ },
+ {
+ "title": "food",
+ "users": [
+ "cbb2f023b6aa09626d51d2f4ea99fa9138ea80ec7d5ffdce9feef8dcd6352031"
+ ]
+ }
+]
+
+