commit 214e45a98b85208375de68ae5da4fee15679d1b7
parent 2a8b9f75c13d4b70cf77adc5d45e124b2749ad96
Author: William Casarin <jb55@jb55.com>
Date: Wed, 25 Jan 2023 12:50:04 -0800
Add muting and mutelists
- Filter muted posts from feed on mute
- List muted users in sidebar
Changelog-Added: Added ability to block users
Diffstat:
14 files changed, 349 insertions(+), 39 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -159,6 +159,9 @@
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD529817F5B00D66079 /* ReportView.swift */; };
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD72981980C00D66079 /* Lists.swift */; };
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDB2981A19E00D66079 /* ListTests.swift */; };
+ 4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */; };
+ 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE02981A83900D66079 /* MutelistView.swift */; };
+ 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE22981BC7D00D66079 /* UserView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
@@ -395,6 +398,9 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
4CF0ABD72981980C00D66079 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = "<group>"; };
4CF0ABDB2981A19E00D66079 /* ListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = "<group>"; };
+ 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistModel.swift; sourceTree = "<group>"; };
+ 4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; };
+ 4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
@@ -538,6 +544,7 @@
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
+ 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -545,6 +552,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
+ 4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
4CB88394296F7F8100DC99E7 /* Reactions */,
4CB88387296AF97C00DC99E7 /* ActionBar */,
@@ -686,6 +694,7 @@
4CB8838C296F710400DC99E7 /* Reposted.swift */,
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
+ 4CF0ABE22981BC7D00D66079 /* UserView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -775,6 +784,14 @@
name = Frameworks;
sourceTree = "<group>";
};
+ 4CF0ABDF2981A83000D66079 /* Muting */ = {
+ isa = PBXGroup;
+ children = (
+ 4CF0ABE02981A83900D66079 /* MutelistView.swift */,
+ );
+ path = Muting;
+ sourceTree = "<group>";
+ };
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -969,6 +986,7 @@
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
+ 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -988,6 +1006,7 @@
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
+ 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
@@ -1060,6 +1079,7 @@
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
+ 4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
diff --git a/damus/Components/UserView.swift b/damus/Components/UserView.swift
@@ -0,0 +1,42 @@
+//
+// UserView.swift
+// damus
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import SwiftUI
+
+struct UserView: View {
+ let damus_state: DamusState
+ let pubkey: String
+
+ var body: some View {
+ let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
+ let followers = FollowersModel(damus_state: damus_state, target: pubkey)
+ let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
+
+ NavigationLink(destination: pv) {
+ ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
+
+ VStack(alignment: .leading) {
+ let profile = damus_state.profiles.lookup(id: pubkey)
+ ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
+ if let about = profile?.about {
+ Text(about)
+ .lineLimit(3)
+ .font(.footnote)
+ }
+ }
+
+ Spacer()
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+struct UserView_Previews: PreviewProvider {
+ static var previews: some View {
+ UserView(damus_state: test_damus_state(), pubkey: "pk")
+ }
+}
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -81,6 +81,10 @@ struct ContentView: View {
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
+ @State var blocking: String? = nil
+ @State var confirm_block: Bool = false
+ @State var user_blocked_confirm: Bool = false
+ @State var confirm_overwrite_mutelist: Bool = false
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@@ -348,6 +352,11 @@ struct ContentView: View {
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
}
+ .onReceive(handle_notify(.block)) { notif in
+ let pubkey = notif.object as! String
+ self.blocking = pubkey
+ self.confirm_block = true
+ }
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
self.damus_state?.pool.send(.event(ev))
@@ -422,6 +431,91 @@ struct ContentView: View {
.onReceive(timer) { n in
self.damus_state?.pool.connect_to_disconnected()
}
+ .onReceive(handle_notify(.new_mutes)) { notif in
+ home.filter_muted()
+ }
+ .alert("User blocked", isPresented: $user_blocked_confirm, actions: {
+ Button("Thanks!") {
+ user_blocked_confirm = false
+ }
+ }, message: {
+ if let pubkey = self.blocking {
+ let profile = damus_state!.profiles.lookup(id: pubkey)
+ let name = Profile.displayName(profile: profile, pubkey: pubkey)
+ Text("\(name) has been blocked")
+ } else {
+ Text("User has been blocked")
+ }
+ })
+ .alert("Create new mutelist", isPresented: $confirm_overwrite_mutelist, actions: {
+ Button("Yes, Overwrite") {
+ guard let ds = damus_state else {
+ return
+ }
+
+ guard let keypair = ds.keypair.to_full() else {
+ return
+ }
+
+ guard let pubkey = blocking else {
+ return
+ }
+
+ guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
+ return
+ }
+
+ damus_state?.contacts.set_mutelist(mutelist)
+ ds.pool.send(.event(mutelist))
+
+ confirm_overwrite_mutelist = false
+ confirm_block = false
+ user_blocked_confirm = true
+ }
+
+ Button("Cancel") {
+ confirm_overwrite_mutelist = false
+ confirm_block = false
+ }
+ }, message: {
+ Text("No block list found, create a new one? This will overwrite any previous block lists.")
+ })
+ .alert("Block User", isPresented: $confirm_block, actions: {
+ Button("Block") {
+ guard let ds = damus_state else {
+ return
+ }
+
+ if ds.contacts.mutelist == nil {
+ confirm_overwrite_mutelist = true
+ } else {
+ guard let keypair = ds.keypair.to_full() else {
+ return
+ }
+ guard let pubkey = blocking else {
+ return
+ }
+
+ guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
+ return
+ }
+ damus_state?.contacts.set_mutelist(ev)
+ ds.pool.send(.event(ev))
+ }
+ }
+
+ Button("Cancel") {
+ confirm_block = false
+ }
+ }, message: {
+ if let pubkey = blocking {
+ let profile = damus_state?.profiles.lookup(id: pubkey)
+ let name = Profile.displayName(profile: profile, pubkey: pubkey)
+ Text("Block \(name)?")
+ } else {
+ Text("Could not find user to block...")
+ }
+ })
}
func switch_timeline(_ timeline: Timeline) {
diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift
@@ -11,13 +11,51 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
+ private var muted: Set<String> = Set()
+
let our_pubkey: String
var event: NostrEvent?
+ var mutelist: NostrEvent?
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
+ func is_muted(_ pk: String) -> Bool {
+ return muted.contains(pk)
+ }
+
+ func set_mutelist(_ ev: NostrEvent) {
+ let oldlist = self.mutelist
+ self.mutelist = ev
+
+ let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
+ let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
+ let diff = old.symmetricDifference(new)
+
+ var new_mutes = Array<String>()
+ var new_unmutes = Array<String>()
+
+ for d in diff {
+ if new.contains(d) {
+ new_mutes.append(d)
+ } else {
+ new_unmutes.append(d)
+ }
+ }
+
+ // TODO: set local mutelist here
+ self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
+
+ if new_mutes.count > 0 {
+ notify(.new_mutes, new_mutes)
+ }
+
+ if new_unmutes.count > 0 {
+ notify(.new_unmutes, new_unmutes)
+ }
+ }
+
func get_friendosphere() -> [String] {
var fs = get_friend_list()
fs.append(contentsOf: get_friend_of_friend_list())
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
@@ -98,6 +98,8 @@ class HomeModel: ObservableObject {
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
+ case .list:
+ handle_list_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
@@ -124,6 +126,12 @@ class HomeModel: ObservableObject {
func handle_channel_meta(_ ev: NostrEvent) {
}
+ func filter_muted() {
+ self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) }
+ self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
+ self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
+ }
+
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -274,7 +282,11 @@ class HomeModel: ObservableObject {
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
our_contacts_filter.authors = [damus_state.pubkey]
-
+
+ var our_blocklist_filter = NostrFilter.filter_kinds([30000])
+ our_blocklist_filter.parameter = "mute"
+ our_blocklist_filter.authors = [damus_state.pubkey]
+
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
@@ -311,7 +323,7 @@ class HomeModel: ObservableObject {
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
- var contacts_filters = [contacts_filter, our_contacts_filter]
+ var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
@@ -335,7 +347,30 @@ class HomeModel: ObservableObject {
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
}
-
+
+ func handle_list_event(_ ev: NostrEvent) {
+ // we only care about our lists
+ guard ev.pubkey == damus_state.pubkey else {
+ return
+ }
+
+ if let mutelist = damus_state.contacts.mutelist {
+ if ev.created_at <= mutelist.created_at {
+ return
+ }
+ }
+
+ guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
+ return
+ }
+
+ guard name.ref_id == "mute" else {
+ return
+ }
+
+ damus_state.contacts.set_mutelist(ev)
+ }
+
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
@@ -376,6 +411,9 @@ class HomeModel: ObservableObject {
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
+ if damus_state.contacts.is_muted(ev.pubkey) {
+ return true
+ }
return !ev.should_show_event
}
diff --git a/damus/Models/MutelistModel.swift b/damus/Models/MutelistModel.swift
@@ -0,0 +1,18 @@
+//
+// ListModel.swift
+// damus
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import Foundation
+
+
+/*
+ class MutelistModel: ObservableObject {
+ let contacts: Contacts
+
+ @Published var users: [String]
+
+ }
+ */
diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift
@@ -17,6 +17,7 @@ struct NostrFilter: Codable {
var limit: UInt32?
var authors: [String]?
var hashtag: [String]? = nil
+ var parameter: String? = nil
private enum CodingKeys : String, CodingKey {
case ids
@@ -24,6 +25,7 @@ struct NostrFilter: Codable {
case referenced_ids = "#e"
case pubkeys = "#p"
case hashtag = "#t"
+ case parameter = "#d"
case since
case until
case authors
diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift
@@ -19,4 +19,5 @@ enum NostrKind: Int {
case channel_create = 40
case channel_meta = 41
case chat = 42
+ case list = 30000
}
diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift
@@ -86,6 +86,15 @@ extension Notification.Name {
static var report: Notification.Name {
return Notification.Name("report")
}
+ static var block: Notification.Name {
+ return Notification.Name("block")
+ }
+ static var new_mutes: Notification.Name {
+ return Notification.Name("new_mutes")
+ }
+ static var new_unmutes: Notification.Name {
+ return Notification.Name("new_unmutes")
+ }
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift
@@ -44,6 +44,12 @@ struct EventMenuContext: View {
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
}
+
+ Button {
+ notify(.block, event.pubkey)
+ } label: {
+ Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
+ }
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift
@@ -15,26 +15,7 @@ struct FollowUserView: View {
var body: some View {
HStack {
- let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
- let followers = FollowersModel(damus_state: damus_state, target: target.pubkey)
- let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
-
- NavigationLink(destination: pv) {
- ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
-
- VStack(alignment: .leading) {
- let profile = damus_state.profiles.lookup(id: target.pubkey)
- ProfileName(pubkey: target.pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
- if let about = profile?.about {
- Text(FollowUserView.markdown.process(about))
- .lineLimit(3)
- .font(.footnote)
- }
- }
-
- Spacer()
- }
- .buttonStyle(PlainButtonStyle())
+ UserView(damus_state: damus_state, pubkey: target.pubkey)
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift
@@ -0,0 +1,67 @@
+//
+// MutelistView.swift
+// damus
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import SwiftUI
+
+struct MutelistView: View {
+ let damus_state: DamusState
+ @State var users: [String]
+
+ func RemoveAction(pubkey: String) -> some View {
+ Button {
+ guard let mutelist = damus_state.contacts.mutelist else {
+ return
+ }
+
+ guard let keypair = damus_state.keypair.to_full() else {
+ return
+ }
+
+ guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: pubkey) else {
+ return
+ }
+
+ damus_state.contacts.set_mutelist(new_ev)
+ damus_state.pool.send(.event(new_ev))
+ users = get_mutelist_users(new_ev)
+ } label: {
+ Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
+ }
+ .tint(.red)
+ }
+
+
+ var body: some View {
+ List(users, id: \.self) { pubkey in
+ UserView(damus_state: damus_state, pubkey: pubkey)
+ .id(pubkey)
+ .swipeActions {
+ RemoveAction(pubkey: pubkey)
+ }
+ }
+ .navigationTitle("Blocked Users")
+ }
+}
+
+
+func get_mutelist_users(_ mlist: NostrEvent?) -> [String] {
+ guard let mutelist = mlist else {
+ return []
+ }
+
+ return mutelist.tags.reduce(into: Array<String>()) { pks, tag in
+ if tag.count >= 2 && tag[0] == "p" {
+ pks.append(tag[1])
+ }
+ }
+}
+
+struct MutelistView_Previews: PreviewProvider {
+ static var previews: some View {
+ MutelistView(damus_state: test_damus_state(), users: [test_event.pubkey, test_event.pubkey+"hi"])
+ }
+}
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -383,6 +383,10 @@ struct ProfileView: View {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
+
+ Button("Block") {
+ notify(.block, profile.pubkey)
+ }
}
.ignoresSafeArea()
}
diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift
@@ -75,22 +75,6 @@ struct SideMenuView: View {
Divider()
.padding(.trailing,40)
-
- /*
- HStack(alignment: .bottom) {
- Text("69,420")
- .foregroundColor(.accentColor)
- .font(.largeTitle)
- Text("SATS")
- .font(.caption)
- .padding(.bottom,6)
- }
-
- Divider()
- .padding(.trailing,40)
- */
-
- // THERE IS A LIMIT OF 10 NAVIGATIONLINKS!!! (Consider some in other views)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
Label(NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
@@ -123,6 +107,12 @@ struct SideMenuView: View {
})
*/
+ NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
+ Label(NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon")
+ .font(.title2)
+ .foregroundColor(textColor())
+ }
+
NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) {
Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
.font(.title2)