refactor: Adding structure
Huge refactor to add better structure to the project. Separating features with their associated view and model structure. This should be better organization and will allow us to improve the overall architecture in the future. I forsee many more improvements that can follow this change. e.g. MVVM Arch As well as cleaning up duplicate, unused, functionality. Many files have global functions that can also be moved or be renamed. damus/ ├── Features/ │ ├── <Feature>/ │ │ ├── Views/ │ │ └── Models/ ├── Shared/ │ ├── Components/ │ ├── Media/ │ ├── Buttons/ │ ├── Extensions/ │ ├── Empty Views/ │ ├── ErrorHandling/ │ ├── Modifiers/ │ └── Utilities/ ├── Core/ │ ├── Nostr/ │ ├── NIPs/ │ ├── DIPs/ │ ├── Types/ │ ├── Networking/ │ └── Storage/ Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
committed by
Daniel D’Aquino
parent
fdbf271432
commit
65a22813a3
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Mute.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: Set<MuteItem>) -> NostrEvent? {
|
||||
let muted_items: Set<MuteItem> = (mprev?.mute_list ?? Set<MuteItem>()).union(to_add).filter { !$0.is_expired() }
|
||||
let tags: [[String]] = muted_items.map { $0.tag }
|
||||
return NostrEvent(content: mprev?.content ?? "", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: tags)
|
||||
}
|
||||
|
||||
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: MuteItem) -> NostrEvent? {
|
||||
return create_or_update_mutelist(keypair: keypair, mprev: mprev, to_add: [to_add])
|
||||
}
|
||||
|
||||
func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_remove: MuteItem) -> NostrEvent? {
|
||||
let muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>()).subtracting([to_remove]).filter { !$0.is_expired() }
|
||||
let tags: [[String]] = muted_items.map { $0.tag }
|
||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: tags)
|
||||
}
|
||||
|
||||
func toggle_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_toggle: MuteItem) -> NostrEvent? {
|
||||
let existing_muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>())
|
||||
|
||||
if existing_muted_items.contains(to_toggle) {
|
||||
// Already exists, remove
|
||||
return remove_from_mutelist(keypair: keypair, prev: prev, to_remove: to_toggle)
|
||||
} else {
|
||||
// Doesn't exist, add
|
||||
return create_or_update_mutelist(keypair: keypair, mprev: prev, to_add: to_toggle)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// MuteItem.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/13/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents an item that is muted.
|
||||
enum MuteItem: Hashable, Equatable {
|
||||
/// A user that is muted.
|
||||
///
|
||||
/// The associated type is the ``Pubkey`` that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case user(Pubkey, Date?)
|
||||
|
||||
/// A hashtag that is muted.
|
||||
///
|
||||
/// The associated type is the hashtag string that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case hashtag(Hashtag, Date?)
|
||||
|
||||
/// A word/phrase that is muted.
|
||||
///
|
||||
/// The associated type is the word/phrase that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case word(String, Date?)
|
||||
|
||||
/// A thread that is muted.
|
||||
///
|
||||
/// The associated type is the `id` of the note that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case thread(NoteId, Date?)
|
||||
|
||||
func is_expired() -> Bool {
|
||||
switch self {
|
||||
case .user(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .hashtag(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .word(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .thread(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: MuteItem, rhs: MuteItem) -> Bool {
|
||||
// lhs is the item we want to check (ie. the item the user is attempting to display)
|
||||
// rhs is the item we want to check against (ie. the item in the mute list)
|
||||
|
||||
switch (lhs, rhs) {
|
||||
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, _)):
|
||||
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
||||
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, _)):
|
||||
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
||||
case (.word(let lhs_word, _), .word(let rhs_word, _)):
|
||||
return lhs_word == rhs_word && !rhs.is_expired()
|
||||
case (.thread(let lhs_thread, _), .thread(let rhs_thread, _)):
|
||||
return lhs_thread == rhs_thread && !rhs.is_expired()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var refTags: [String] {
|
||||
switch self {
|
||||
case .user(let pubkey, _):
|
||||
return RefId.pubkey(pubkey).tag
|
||||
case .hashtag(let hashtag, _):
|
||||
return RefId.hashtag(hashtag).tag
|
||||
case .word(let string, _):
|
||||
return ["word", string]
|
||||
case .thread(let noteId, _):
|
||||
return RefId.event(noteId).tag
|
||||
}
|
||||
}
|
||||
|
||||
var tag: [String] {
|
||||
var tag = self.refTags
|
||||
|
||||
switch self {
|
||||
case .user(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .hashtag(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .word(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .thread(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .user:
|
||||
return "user"
|
||||
case .hashtag:
|
||||
return "hashtag"
|
||||
case .word:
|
||||
return "word"
|
||||
case .thread:
|
||||
return "thread"
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ tag: [String]) {
|
||||
guard let tag_id = tag.first else { return nil }
|
||||
guard let tag_content = tag[safe: 1] else { return nil }
|
||||
|
||||
let tag_expiration_date: Date? = {
|
||||
if let tag_expiration_string: String = tag[safe: 2],
|
||||
let tag_expiration_number: TimeInterval = Double(tag_expiration_string) {
|
||||
return Date(timeIntervalSince1970: tag_expiration_number)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
switch tag_id {
|
||||
case "p":
|
||||
guard let pubkey = Pubkey(hex: tag_content) else { return nil }
|
||||
self = MuteItem.user(pubkey, tag_expiration_date)
|
||||
break
|
||||
case "t":
|
||||
self = MuteItem.hashtag(Hashtag(hashtag: tag_content), tag_expiration_date)
|
||||
break
|
||||
case "word":
|
||||
self = MuteItem.word(tag_content, tag_expiration_date)
|
||||
break
|
||||
case "thread":
|
||||
guard let note_id = NoteId(hex: tag_content) else { return nil }
|
||||
self = MuteItem.thread(note_id, tag_expiration_date)
|
||||
break
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// - MARK: TagConvertible
|
||||
extension MuteItem: TagConvertible {
|
||||
enum MuteKeys: String {
|
||||
case p, t, word, e
|
||||
|
||||
init?(tag: NdbTagElem) {
|
||||
let len = tag.count
|
||||
if len == 1 {
|
||||
switch tag.single_char {
|
||||
case "p": self = .p
|
||||
case "t": self = .t
|
||||
case "e": self = .e
|
||||
default: return nil
|
||||
}
|
||||
} else if len == 4 && tag.matches_str("word", tag_len: 4) {
|
||||
self = .word
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { self.rawValue }
|
||||
}
|
||||
|
||||
static func from_tag(tag: TagSequence) -> MuteItem? {
|
||||
guard tag.count >= 2 else { return nil }
|
||||
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard let t0 = i.next(),
|
||||
let mkey = MuteKeys(tag: t0),
|
||||
let t1 = i.next()
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expiry: Date? = nil
|
||||
if let expiry_str = i.next(), let ts = expiry_str.u64() {
|
||||
expiry = Date(timeIntervalSince1970: Double(ts))
|
||||
}
|
||||
|
||||
switch mkey {
|
||||
case .p:
|
||||
return t1.id().map({ .user(Pubkey($0), expiry) })
|
||||
case .t:
|
||||
return .hashtag(Hashtag(hashtag: t1.string()), expiry)
|
||||
case .word:
|
||||
return .word(t1.string(), expiry)
|
||||
case .e:
|
||||
guard let id = t1.id() else { return nil }
|
||||
return .thread(NoteId(id), expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// MutedThreadsManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
|
||||
pk_setting_key(pubkey, key: "muted_threads")
|
||||
}
|
||||
|
||||
func loadOldMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
return xs.reduce(into: [NoteId]()) { ids, k in
|
||||
guard let note_id = hex_decode(k) else { return }
|
||||
ids.append(NoteId(Data(note_id)))
|
||||
}
|
||||
}
|
||||
|
||||
// We need to still use it since existing users might have their muted threads stored in UserDefaults
|
||||
// So now all it's doing is moving a users muted threads to the new kind:10000 system
|
||||
// It should not be used for any purpose beyond that
|
||||
func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) {
|
||||
// Ensure that keypair is fullkeypair
|
||||
guard let fullKeypair = keypair.to_full() else { return }
|
||||
// Load existing muted threads
|
||||
let mutedThreads = loadOldMutedThreads(pubkey: fullKeypair.pubkey)
|
||||
guard !mutedThreads.isEmpty else { return }
|
||||
// Set new muted system for those existing threads
|
||||
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||
damus_state.nostrNetwork.postbox.send(new_mutelist_event)
|
||||
// Set existing muted threads to an empty array
|
||||
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// MutelistManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/28/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MutelistManager {
|
||||
let user_keypair: Keypair
|
||||
private(set) var event: NostrEvent? = nil
|
||||
|
||||
var users: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var hashtags: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var threads: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var words: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
|
||||
var muted_notes_cache: [NoteId: EventMuteStatus] = [:]
|
||||
|
||||
init(user_keypair: Keypair) {
|
||||
self.user_keypair = user_keypair
|
||||
}
|
||||
|
||||
func refresh_sets() {
|
||||
guard let referenced_mute_items = event?.referenced_mute_items else { return }
|
||||
|
||||
var new_users: Set<MuteItem> = []
|
||||
var new_hashtags: Set<MuteItem> = []
|
||||
var new_threads: Set<MuteItem> = []
|
||||
var new_words: Set<MuteItem> = []
|
||||
|
||||
for mute_item in referenced_mute_items {
|
||||
switch mute_item {
|
||||
case .user:
|
||||
new_users.insert(mute_item)
|
||||
case .hashtag:
|
||||
new_hashtags.insert(mute_item)
|
||||
case .word:
|
||||
new_words.insert(mute_item)
|
||||
case .thread:
|
||||
new_threads.insert(mute_item)
|
||||
}
|
||||
}
|
||||
|
||||
users = new_users
|
||||
hashtags = new_hashtags
|
||||
threads = new_threads
|
||||
words = new_words
|
||||
}
|
||||
|
||||
func reset_cache() {
|
||||
self.muted_notes_cache = [:]
|
||||
}
|
||||
|
||||
func is_muted(_ item: MuteItem) -> Bool {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
return users.contains(item)
|
||||
case .hashtag(_, _):
|
||||
return hashtags.contains(item)
|
||||
case .word(_, _):
|
||||
return words.contains(item)
|
||||
case .thread(_, _):
|
||||
return threads.contains(item)
|
||||
}
|
||||
}
|
||||
|
||||
func is_event_muted(_ ev: NostrEvent) -> Bool {
|
||||
return self.event_muted_reason(ev) != nil
|
||||
}
|
||||
|
||||
func set_mutelist(_ ev: NostrEvent) {
|
||||
let oldlist = self.event
|
||||
self.event = ev
|
||||
|
||||
let old: Set<MuteItem> = oldlist?.mute_list ?? Set<MuteItem>()
|
||||
let new: Set<MuteItem> = ev.mute_list ?? Set<MuteItem>()
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
var new_mutes = Set<MuteItem>()
|
||||
var new_unmutes = Set<MuteItem>()
|
||||
|
||||
for d in diff {
|
||||
if new.contains(d) {
|
||||
add_mute_item(d)
|
||||
new_mutes.insert(d)
|
||||
} else {
|
||||
remove_mute_item(d)
|
||||
new_unmutes.insert(d)
|
||||
}
|
||||
}
|
||||
|
||||
if new_mutes.count > 0 {
|
||||
notify(.new_mutes(new_mutes))
|
||||
}
|
||||
|
||||
if new_unmutes.count > 0 {
|
||||
notify(.new_unmutes(new_unmutes))
|
||||
}
|
||||
}
|
||||
|
||||
private func add_mute_item(_ item: MuteItem) {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
guard !users.contains(item) else { return }
|
||||
users.insert(item)
|
||||
case .hashtag(_, _):
|
||||
guard !hashtags.contains(item) else { return }
|
||||
hashtags.insert(item)
|
||||
case .word(_, _):
|
||||
guard !words.contains(item) else { return }
|
||||
words.insert(item)
|
||||
case .thread(_, _):
|
||||
guard !threads.contains(item) else { return }
|
||||
threads.insert(item)
|
||||
}
|
||||
}
|
||||
|
||||
private func remove_mute_item(_ item: MuteItem) {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
users.remove(item)
|
||||
case .hashtag(_, _):
|
||||
hashtags.remove(item)
|
||||
case .word(_, _):
|
||||
words.remove(item)
|
||||
case .thread(_, _):
|
||||
threads.remove(item)
|
||||
}
|
||||
}
|
||||
|
||||
func event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
|
||||
if let cached_mute_status = self.muted_notes_cache[ev.id] {
|
||||
return cached_mute_status.mute_reason()
|
||||
}
|
||||
if let reason = self.compute_event_muted_reason(ev) {
|
||||
self.muted_notes_cache[ev.id] = .muted(reason: reason)
|
||||
return reason
|
||||
}
|
||||
self.muted_notes_cache[ev.id] = .not_muted
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Check if an event is muted given a collection of ``MutedItem``.
|
||||
///
|
||||
/// - Parameter ev: The ``NostrEvent`` that you want to check the muted reason for.
|
||||
/// - Returns: The ``MuteItem`` that matched the event. Or `nil` if the event is not muted.
|
||||
func compute_event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
|
||||
// Events from the current user should not be muted.
|
||||
guard self.user_keypair.pubkey != ev.pubkey else { return nil }
|
||||
|
||||
// Check if user is muted
|
||||
let check_user_item = MuteItem.user(ev.pubkey, nil)
|
||||
if users.contains(check_user_item) {
|
||||
return check_user_item
|
||||
}
|
||||
|
||||
// Check if hashtag is muted
|
||||
for hashtag in ev.referenced_hashtags {
|
||||
let check_hashtag_item = MuteItem.hashtag(hashtag, nil)
|
||||
if hashtags.contains(check_hashtag_item) {
|
||||
return check_hashtag_item
|
||||
}
|
||||
}
|
||||
|
||||
// Check if thread is muted
|
||||
for thread_id in ev.referenced_ids {
|
||||
let check_thread_item = MuteItem.thread(thread_id, nil)
|
||||
if threads.contains(check_thread_item) {
|
||||
return check_thread_item
|
||||
}
|
||||
}
|
||||
|
||||
// Check if word is muted
|
||||
if let content: String = ev.maybe_get_content(self.user_keypair)?.lowercased() {
|
||||
for word in words {
|
||||
if case .word(let string, _) = word {
|
||||
if content.contains(string.lowercased()) {
|
||||
return word
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
enum EventMuteStatus {
|
||||
case muted(reason: MuteItem)
|
||||
case not_muted
|
||||
|
||||
func mute_reason() -> MuteItem? {
|
||||
switch self {
|
||||
case .muted(reason: let reason):
|
||||
return reason
|
||||
case .not_muted:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// AddMuteItemView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/10/24.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct AddMuteItemView: View {
|
||||
let state: DamusState
|
||||
@Binding var new_text: String
|
||||
@State var expiration: DamusDuration = .indefinite
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var trimmedText: String {
|
||||
new_text.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
let trimmedText = self.trimmedText
|
||||
|
||||
HStack {
|
||||
Label("", image: "copy2")
|
||||
.onTapGesture {
|
||||
if let pasted_text = UIPasteboard.general.string {
|
||||
self.new_text = pasted_text.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
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(trimmedText.isEmpty ? 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 trimmedText.starts(with: "npub") {
|
||||
if let pubkey: Pubkey = bech32_pubkey_decode(trimmedText) {
|
||||
return .user(pubkey, expiration_date)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if trimmedText.starts(with: "#") {
|
||||
// Remove the starting `#` character
|
||||
return .hashtag(Hashtag(hashtag: String("\(trimmedText)".dropFirst())), expiration_date)
|
||||
} else {
|
||||
return .word(trimmedText, 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.nostrNetwork.postbox.send(mutelist)
|
||||
}
|
||||
|
||||
new_text = ""
|
||||
|
||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Add mute item", comment: "Button to an add an item to the user's mutelist.")
|
||||
.bold()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
.padding(.vertical)
|
||||
.opacity(trimmedText.isEmpty ? 0.5 : 1.0)
|
||||
.disabled(trimmedText.isEmpty)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct AddMuteItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddMuteItemView(state: test_damus_state, new_text: .constant(""))
|
||||
}
|
||||
}
|
||||
@@ -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(verbatim: "Mute hashtag")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// MutelistView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MutelistView: View {
|
||||
let damus_state: DamusState
|
||||
@State var show_add_muteitem: Bool = false
|
||||
|
||||
@State var users: [MuteItem] = []
|
||||
@State var hashtags: [MuteItem] = []
|
||||
@State var threads: [MuteItem] = []
|
||||
@State var words: [MuteItem] = []
|
||||
|
||||
@State var new_text: String = ""
|
||||
|
||||
func RemoveAction(item: MuteItem) -> some View {
|
||||
Button {
|
||||
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: item)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||
damus_state.nostrNetwork.postbox.send(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 {
|
||||
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(verbatim: "#\(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("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(
|
||||
header: Text(NSLocalizedString("Users", comment: "Section header title for a list of muted users.")),
|
||||
footer: Text("").padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Muted", comment: "Navigation title of view to see list of muted users & phrases."))
|
||||
.onAppear {
|
||||
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 }) {
|
||||
AddMuteItemView(state: damus_state, new_text: $new_text)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MutelistView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MutelistView(damus_state: test_damus_state)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user