Merge improved mute functionality from Charlie
This merge adds a bunch of new features from charlie's work on the new mutelist changes: - Muted words - Mute performance optimizations - New mute list UI I needed to make a few changes to fix the tests in this merge. Otherwise it seems to work ok! Thank to Charlie for getting all of this working after many rounds of review! * branch `mute` of https://github.com/damus-io/damus: mute: fix bug with duplicate Indefinite items in MuteDurationMenu mute: fix mute hashtag from search view if no existing mutelist mute: integrate new MutelistManager mute: adding MutelistManager.swift mute: add maybe_get_content function to NdbNote mute: fix bug where mutes can't be added without existing mutelist mute: fix issue with not being able to change mute duration mute: don't mutate string when adding hashtag mute: implement fast MuteItem decoder tags: add u64 decoding function mute: migrating muted_threads to new mute list mute: adding ability to mute hashtag from SearchView mute: updating UI to support new mute list mute: adding filtering support for MuteItem events mute: receiving New Mute List Type mute: migrate Lists.swift to use new MuteItem mute: add new UI views for new mute list mute: adding new structs/enums for new mute list Changelog-Added: Add ability to mute words, add new mutelist interface (Charlie)
This commit is contained in:
@@ -20,9 +20,9 @@ struct DMChatView: View, KeyboardReadable {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0, keypair: damus_state.keypair)}, id: \.0.id) { (ev, ind) in
|
||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
|
||||
.contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
|
||||
}
|
||||
EndBlock(height: 1)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ struct DirectMessagesView: View {
|
||||
|
||||
func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] {
|
||||
return dms.filter({ dm in
|
||||
return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(dm.pubkey)
|
||||
return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.mutelist_manager.is_muted(.user(dm.pubkey, nil))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ struct DirectMessagesView: View {
|
||||
|
||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||
Group {
|
||||
if let ev = model.events.last {
|
||||
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0, keypair: damus_state.keypair) }) {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
.onTapGesture {
|
||||
self.model.set_active_dm_model(model)
|
||||
|
||||
@@ -8,21 +8,15 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EventMenuContext: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let keypair: Keypair
|
||||
let target_pubkey: Pubkey
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
let profileModel : ProfileModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
self.damus_state = damus
|
||||
self.event = event
|
||||
self.keypair = damus.keypair
|
||||
self.target_pubkey = event.pubkey
|
||||
self.bookmarks = damus.bookmarks
|
||||
self.muted_threads = damus.muted_threads
|
||||
self._settings = ObservedObject(wrappedValue: damus.settings)
|
||||
self.profileModel = ProfileModel(pubkey: target_pubkey, damus: damus)
|
||||
}
|
||||
|
||||
@@ -34,7 +28,7 @@ struct EventMenuContext: View {
|
||||
// Add our Menu button inside an overlay modifier to avoid affecting the rest of the layout around us.
|
||||
.overlay(
|
||||
Menu {
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings, profileModel: profileModel)
|
||||
MenuItems(damus_state: damus_state, event: event, target_pubkey: target_pubkey, profileModel: profileModel)
|
||||
} label: {
|
||||
Color.clear
|
||||
}
|
||||
@@ -49,38 +43,31 @@ struct EventMenuContext: View {
|
||||
}
|
||||
|
||||
struct MenuItems: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let keypair: Keypair
|
||||
let target_pubkey: Pubkey
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
let profileModel: ProfileModel
|
||||
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@State private var isBookmarked: Bool = false
|
||||
@State private var isMutedThread: Bool = false
|
||||
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: Pubkey, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager, settings: UserSettingsStore, profileModel: ProfileModel) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
init(damus_state: DamusState, event: NostrEvent, target_pubkey: Pubkey, profileModel: ProfileModel) {
|
||||
let bookmarked = damus_state.bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
let muted_thread = muted_threads.isMutedThread(event, keypair: keypair)
|
||||
let muted_thread = damus_state.mutelist_manager.is_event_muted(event)
|
||||
self._isMutedThread = State(initialValue: muted_thread)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.muted_threads = muted_threads
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.keypair = keypair
|
||||
self.target_pubkey = target_pubkey
|
||||
self.settings = settings
|
||||
self.profileModel = profileModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Button {
|
||||
UIPasteboard.general.string = event.get_content(keypair)
|
||||
UIPasteboard.general.string = event.get_content(damus_state.keypair)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2")
|
||||
}
|
||||
@@ -97,7 +84,7 @@ struct MenuItems: View {
|
||||
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
||||
}
|
||||
|
||||
if settings.developer_mode {
|
||||
if damus_state.settings.developer_mode {
|
||||
Button {
|
||||
UIPasteboard.general.string = event_to_json(ev: event)
|
||||
} label: {
|
||||
@@ -106,8 +93,8 @@ struct MenuItems: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
self.bookmarks.updateBookmark(event)
|
||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
||||
self.damus_state.bookmarks.updateBookmark(event)
|
||||
isBookmarked = self.damus_state.bookmarks.isBookmarked(event)
|
||||
} label: {
|
||||
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.")
|
||||
@@ -122,9 +109,13 @@ struct MenuItems: View {
|
||||
}
|
||||
// Mute thread - relocated to below Broadcast, as to move further away from Add Bookmark to prevent accidental muted threads
|
||||
if event.known_kind != .dm {
|
||||
Button {
|
||||
self.muted_threads.updateMutedThread(event)
|
||||
let muted = self.muted_threads.isMutedThread(event, keypair: self.keypair)
|
||||
MuteDurationMenu { duration in
|
||||
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) {
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||
damus_state.postbox.send(new_mutelist_ev)
|
||||
}
|
||||
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
||||
isMutedThread = muted
|
||||
} label: {
|
||||
let imageName = isMutedThread ? "mute" : "mute"
|
||||
@@ -134,15 +125,15 @@ struct MenuItems: View {
|
||||
}
|
||||
}
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
||||
if damus_state.keypair.pubkey != target_pubkey && damus_state.keypair.privkey != nil {
|
||||
Button(role: .destructive) {
|
||||
notify(.report(.note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))))
|
||||
} label: {
|
||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
notify(.mute(target_pubkey))
|
||||
MuteDurationMenu { duration in
|
||||
notify(.mute(.user(target_pubkey, duration?.date_from_now)))
|
||||
} label: {
|
||||
Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
|
||||
}
|
||||
|
||||
@@ -9,20 +9,25 @@ import SwiftUI
|
||||
|
||||
/// A container view that shows or hides provided content based on whether the given event should be muted or not, with built-in user controls to show or hide content, and an option to customize the muted box
|
||||
struct EventMutingContainerView<Content: View>: View {
|
||||
typealias MuteBoxViewClosure = ((_ shown: Binding<Bool>) -> AnyView)
|
||||
|
||||
typealias MuteBoxViewClosure = ((_ shown: Binding<Bool>, _ mutedReason: MuteItem?) -> AnyView)
|
||||
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let content: Content
|
||||
var customMuteBox: MuteBoxViewClosure?
|
||||
|
||||
/// Represents if the note itself should be shown.
|
||||
///
|
||||
/// By default this is the same as `should_show_event`. However, if the user taps the button to manually show a muted note, this can become out of sync with `should_show_event`.
|
||||
@State var shown: Bool
|
||||
|
||||
|
||||
@State var muted_reason: MuteItem?
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, @ViewBuilder content: () -> Content) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.content = content()
|
||||
self._shown = State(initialValue: should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event))
|
||||
self._shown = State(initialValue: should_show_event(state: damus_state, ev: event))
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, muteBox: @escaping MuteBoxViewClosure, @ViewBuilder content: () -> Content) {
|
||||
@@ -31,17 +36,17 @@ struct EventMutingContainerView<Content: View>: View {
|
||||
}
|
||||
|
||||
var should_mute: Bool {
|
||||
return !should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event)
|
||||
return !should_show_event(state: damus_state, ev: event)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if should_mute {
|
||||
if let customMuteBox {
|
||||
customMuteBox($shown)
|
||||
customMuteBox($shown, muted_reason)
|
||||
}
|
||||
else {
|
||||
EventMutedBoxView(shown: $shown)
|
||||
EventMutedBoxView(shown: $shown, reason: muted_reason)
|
||||
}
|
||||
}
|
||||
if shown {
|
||||
@@ -49,13 +54,16 @@ struct EventMutingContainerView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { mutes in
|
||||
if mutes.contains(event.pubkey) {
|
||||
let new_muted_event_reason = damus_state.mutelist_manager.event_muted_reason(event)
|
||||
if new_muted_event_reason != nil {
|
||||
shown = false
|
||||
muted_reason = new_muted_event_reason
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.new_unmutes)) { unmutes in
|
||||
if unmutes.contains(event.pubkey) {
|
||||
if damus_state.mutelist_manager.event_muted_reason(event) != nil {
|
||||
shown = true
|
||||
muted_reason = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,16 +72,21 @@ struct EventMutingContainerView<Content: View>: View {
|
||||
/// A box that instructs the user about a content that has been muted.
|
||||
struct EventMutedBoxView: View {
|
||||
@Binding var shown: Bool
|
||||
|
||||
var reason: MuteItem?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
|
||||
HStack {
|
||||
Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.")
|
||||
if let reason {
|
||||
Text("Note from a \(reason.title) you've muted", comment: "Text to indicate that what is being shown is a note which has been muted.")
|
||||
} else {
|
||||
Text("Note you've muted", comment: "Text to indicate that what is being shown is a note which has been muted.")
|
||||
}
|
||||
Spacer()
|
||||
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) {
|
||||
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note which has been muted.") : NSLocalizedString("Show", comment: "Button to show a note which has been muted.")) {
|
||||
shown.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
113
damus/Views/Muting/AddMuteItemView.swift
Normal file
113
damus/Views/Muting/AddMuteItemView.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// AddMuteItemView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/10/24.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct AddMuteItemView: View {
|
||||
let state: DamusState
|
||||
@State var new_text: String = ""
|
||||
@State var expiration: DamusDuration = .indefinite
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Add mute item", comment: "Title text to indicate user to an add an item to their mutelist.")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.padding(.vertical)
|
||||
|
||||
Divider()
|
||||
.padding(.bottom)
|
||||
|
||||
Picker(selection: $expiration) {
|
||||
ForEach(DamusDuration.allCases, id: \.self) { duration in
|
||||
Text(duration.title).tag(duration)
|
||||
}
|
||||
} label: {
|
||||
Text("Duration", comment: "The duration in which to mute the given item.")
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Label("", image: "copy2")
|
||||
.onTapGesture {
|
||||
if let pasted_text = UIPasteboard.general.string {
|
||||
self.new_text = pasted_text
|
||||
}
|
||||
}
|
||||
TextField(NSLocalizedString("npub, #hashtag, phrase", comment: "Placeholder example for relay server address."), text: $new_text)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
Label("", image: "close-circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.opacity((new_text == "") ? 0.0 : 1.0)
|
||||
.onTapGesture {
|
||||
self.new_text = ""
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(.secondary.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
|
||||
Button(action: {
|
||||
let expiration_date: Date? = self.expiration.date_from_now
|
||||
let mute_item: MuteItem? = {
|
||||
if new_text.starts(with: "npub") {
|
||||
if let pubkey: Pubkey = bech32_pubkey_decode(new_text) {
|
||||
return .user(pubkey, expiration_date)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if new_text.starts(with: "#") {
|
||||
// Remove the starting `#` character
|
||||
return .hashtag(Hashtag(hashtag: String("\(new_text)".dropFirst())), expiration_date)
|
||||
} else {
|
||||
return .word(new_text, expiration_date)
|
||||
}
|
||||
}()
|
||||
|
||||
// Actually update & relay the new mute list
|
||||
if let mute_item {
|
||||
let existing_mutelist = state.mutelist_manager.event
|
||||
|
||||
guard
|
||||
let full_keypair = state.keypair.to_full(),
|
||||
let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
state.mutelist_manager.set_mutelist(mutelist)
|
||||
state.postbox.send(mutelist)
|
||||
}
|
||||
|
||||
new_text = ""
|
||||
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text(verbatim: "Add mute item")
|
||||
.bold()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct AddMuteItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddMuteItemView(state: test_damus_state)
|
||||
}
|
||||
}
|
||||
35
damus/Views/Muting/MuteDurationMenu.swift
Normal file
35
damus/Views/Muting/MuteDurationMenu.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MuteDurationMenu.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/14/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MuteDurationMenu<T: View>: View {
|
||||
var action: (DamusDuration?) -> Void
|
||||
@ViewBuilder var label: () -> T
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
ForEach(DamusDuration.allCases, id: \.self) { duration in
|
||||
Button {
|
||||
action(duration)
|
||||
} label: {
|
||||
Text("\(duration.title)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
self.label()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MuteDurationMenu { _ in
|
||||
|
||||
} label: {
|
||||
Text("Mute hashtag")
|
||||
}
|
||||
}
|
||||
@@ -9,55 +9,130 @@ import SwiftUI
|
||||
|
||||
struct MutelistView: View {
|
||||
let damus_state: DamusState
|
||||
@State var users: [Pubkey]
|
||||
|
||||
func RemoveAction(pubkey: Pubkey) -> some View {
|
||||
@State var show_add_muteitem: Bool = false
|
||||
|
||||
@State var users: [MuteItem] = []
|
||||
@State var hashtags: [MuteItem] = []
|
||||
@State var threads: [MuteItem] = []
|
||||
@State var words: [MuteItem] = []
|
||||
|
||||
func RemoveAction(item: MuteItem) -> some View {
|
||||
Button {
|
||||
guard let mutelist = damus_state.contacts.mutelist,
|
||||
guard let mutelist = damus_state.mutelist_manager.event,
|
||||
let keypair = damus_state.keypair.to_full(),
|
||||
let new_ev = remove_from_mutelist(keypair: keypair,
|
||||
prev: mutelist,
|
||||
to_remove: .pubkey(pubkey))
|
||||
to_remove: item)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.contacts.set_mutelist(new_ev)
|
||||
|
||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||
damus_state.postbox.send(new_ev)
|
||||
users = get_mutelist_users(new_ev)
|
||||
updateMuteItems()
|
||||
} label: {
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
|
||||
|
||||
func updateMuteItems() {
|
||||
users = Array(damus_state.mutelist_manager.users)
|
||||
hashtags = Array(damus_state.mutelist_manager.hashtags)
|
||||
threads = Array(damus_state.mutelist_manager.threads)
|
||||
words = Array(damus_state.mutelist_manager.words)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(users, id: \.self) { pubkey in
|
||||
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
||||
.id(pubkey)
|
||||
.swipeActions {
|
||||
RemoveAction(pubkey: pubkey)
|
||||
List {
|
||||
Section(NSLocalizedString("Users", comment: "Section header title for a list of muted users.")) {
|
||||
ForEach(users, id: \.self) { user in
|
||||
if case let MuteItem.user(pubkey, _) = user {
|
||||
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
||||
.id(pubkey)
|
||||
.swipeActions {
|
||||
RemoveAction(item: .user(pubkey, nil))
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||
}
|
||||
Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) {
|
||||
ForEach(hashtags, id: \.self) { item in
|
||||
if case let MuteItem.hashtag(hashtag, _) = item {
|
||||
Text("#\(hashtag.hashtag)")
|
||||
.id(hashtag.hashtag)
|
||||
.swipeActions {
|
||||
RemoveAction(item: .hashtag(hashtag, nil))
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.Search(search: SearchModel.init(state: damus_state, search: NostrFilter(hashtag: [hashtag.hashtag]))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) {
|
||||
ForEach(words, id: \.self) { item in
|
||||
if case let MuteItem.word(word, _) = item {
|
||||
Text("\(word)")
|
||||
.id(word)
|
||||
.swipeActions {
|
||||
RemoveAction(item: .word(word, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")) {
|
||||
ForEach(threads, id: \.self) { item in
|
||||
if case let MuteItem.thread(note_id, _) = item {
|
||||
if let event = damus_state.events.lookup(note_id) {
|
||||
EventView(damus: damus_state, event: event)
|
||||
.id(note_id.hex())
|
||||
.swipeActions {
|
||||
RemoveAction(item: .thread(note_id, nil))
|
||||
}
|
||||
} else {
|
||||
Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
|
||||
.navigationTitle(NSLocalizedString("Muted", comment: "Navigation title of view to see list of muted users & phrases."))
|
||||
.onAppear {
|
||||
users = get_mutelist_users(damus_state.contacts.mutelist)
|
||||
updateMuteItems()
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { new_mutes in
|
||||
updateMuteItems()
|
||||
}
|
||||
.onReceive(handle_notify(.new_unmutes)) { new_unmutes in
|
||||
updateMuteItems()
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
self.show_add_muteitem = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
|
||||
if #available(iOS 16.0, *) {
|
||||
AddMuteItemView(state: damus_state)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
AddMuteItemView(state: damus_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func get_mutelist_users(_ mutelist: NostrEvent?) -> Array<Pubkey> {
|
||||
guard let mutelist else { return [] }
|
||||
return Array(mutelist.referenced_pubkeys)
|
||||
}
|
||||
|
||||
struct MutelistView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MutelistView(damus_state: test_damus_state, users: [test_note.pubkey, test_note.pubkey])
|
||||
MutelistView(damus_state: test_damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,25 +179,28 @@ struct ProfileView: View {
|
||||
notify(.report(.user(profile.pubkey)))
|
||||
}
|
||||
|
||||
if damus_state.contacts.is_muted(profile.pubkey) {
|
||||
if damus_state.mutelist_manager.is_muted(.user(profile.pubkey, nil)) {
|
||||
Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) {
|
||||
guard
|
||||
let keypair = damus_state.keypair.to_full(),
|
||||
let mutelist = damus_state.contacts.mutelist
|
||||
let mutelist = damus_state.mutelist_manager.event
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(profile.pubkey)) else {
|
||||
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(profile.pubkey, nil)) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.contacts.set_mutelist(new_ev)
|
||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||
damus_state.postbox.send(new_ev)
|
||||
}
|
||||
} else {
|
||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) {
|
||||
notify(.mute(profile.pubkey))
|
||||
MuteDurationMenu { duration in
|
||||
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
|
||||
} label: {
|
||||
Text(NSLocalizedString("Mute", comment: "Button to mute a profile."))
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ struct RepostedEvent: View {
|
||||
EventMutingContainerView(
|
||||
damus_state: damus,
|
||||
event: inner_ev,
|
||||
muteBox: { event_shown in
|
||||
muteBox: { event_shown, muted_reason in
|
||||
AnyView(
|
||||
EventMutedBoxView(shown: event_shown)
|
||||
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||
.padding(.horizontal, 5) // Add a bit of horizontal padding to avoid the mute box from touching the edges of the screen
|
||||
)
|
||||
}) {
|
||||
|
||||
@@ -59,7 +59,8 @@ struct SearchHomeView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) {
|
||||
let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
|
||||
if event_muted {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ struct SearchView: View {
|
||||
let appstate: DamusState
|
||||
@ObservedObject var search: SearchModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@State var is_hashtag_muted: Bool = false
|
||||
|
||||
var content_filter: (NostrEvent) -> Bool {
|
||||
let filters = ContentFilters.defaults(damus_state: self.appstate)
|
||||
return ContentFilters(filters: filters).filter
|
||||
@@ -41,7 +42,69 @@ struct SearchView: View {
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
search.filter_muted()
|
||||
|
||||
if let hashtag_string = search.search.hashtag?.first,
|
||||
notif.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) {
|
||||
is_hashtag_muted = true
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.new_unmutes)) { unmutes in
|
||||
if let hashtag_string = search.search.hashtag?.first,
|
||||
unmutes.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) {
|
||||
is_hashtag_muted = false
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
if let hashtag = search.search.hashtag?.first {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
if is_hashtag_muted {
|
||||
Button {
|
||||
guard
|
||||
let full_keypair = appstate.keypair.to_full(),
|
||||
let existing_mutelist = appstate.mutelist_manager.event,
|
||||
let mutelist = remove_from_mutelist(keypair: full_keypair, prev: existing_mutelist, to_remove: .hashtag(Hashtag(hashtag: hashtag), nil))
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||
appstate.postbox.send(mutelist)
|
||||
} label: {
|
||||
Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.")
|
||||
}
|
||||
} else {
|
||||
MuteDurationMenu { duration in
|
||||
mute_hashtag(hashtag_string: hashtag, expiration_time: duration?.date_from_now)
|
||||
} label: {
|
||||
Text("Mute Hashtag", comment: "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let hashtag_string = search.search.hashtag?.first {
|
||||
is_hashtag_muted = (appstate.mutelist_manager.event?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mute_hashtag(hashtag_string: String, expiration_time: Date?) {
|
||||
let existing_mutelist = appstate.mutelist_manager.event
|
||||
|
||||
guard
|
||||
let full_keypair = appstate.keypair.to_full(),
|
||||
let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time))
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||
appstate.postbox.send(mutelist)
|
||||
}
|
||||
|
||||
var described_search: DescribedSearch {
|
||||
|
||||
@@ -66,7 +66,7 @@ struct SideMenuView: View {
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
|
||||
NavigationLink(value: Route.MuteList) {
|
||||
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,9 @@ struct ThreadView: View {
|
||||
EventMutingContainerView(
|
||||
damus_state: state,
|
||||
event: self.thread.event,
|
||||
muteBox: { event_shown in
|
||||
muteBox: { event_shown, muted_reason in
|
||||
AnyView(
|
||||
EventMutedBoxView(shown: event_shown)
|
||||
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||
.padding(5)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user