This patch achieves the following major things:
- Moving to kind:10000 mute lists
- Allowing for hashtag & keyword muting
- Adding support for mute expirations
A few high level concepts:
- I completely removed the MutedThreadsManager. I replaced it with a single function that will migrate a users existing muted threads to the new kind:10000 mute list
- Added a MuteItem enum (this is a key entry point to the entire patch)
- This patch does not contain support for encrypted mute items. This patch felt like it was getting long enough, and I really just want to get it across the finish line at this point. While encrypted mute items aren’t supported, Damus will relay them when adding or updating your mute list on Damus (ie. Damus won’t break your encrypted mute list items, but also won’t act upon them). I created a ticket for this work:
https://github.com/damus-io/damus/issues/1880
Closes:
https://github.com/damus-io/damus/issues/1718
Closes:
https://github.com/damus-io/damus/issues/856
Lighting Address:
fishc...@strike.me
Signed-off-by: Charlie Fish <con...@charlie.fish>
---
.../NotificationExtensionState.swift | 2 -
damus.xcodeproj/project.pbxproj | 72 +++--
damus/ContentView.swift | 25 +-
damus/Models/Contacts+.swift | 2 +-
damus/Models/Contacts.swift | 19 +-
damus/Models/ContentFilters.swift | 2 +-
damus/Models/DamusState.swift | 5 +-
damus/Models/HeadlessDamusState.swift | 1 -
damus/Models/HomeModel.swift | 303 ++++++++++++++++--
damus/Models/MuteItem.swift | 208 ++++++++++++
damus/Models/MutedThreadsManager.swift | 70 +---
damus/Models/NotificationsManager.swift | 11 +-
damus/Models/SearchHomeModel.swift | 4 +-
damus/Models/SearchModel.swift | 4 +-
damus/Nostr/NostrEvent.swift | 14 +
damus/Nostr/NostrKind.swift | 3 +-
damus/Nostr/ReferencedId.swift | 6 +-
damus/Notify/MuteNotify.swift | 6 +-
damus/Notify/NewMutesNotify.swift | 4 +-
damus/Notify/NewUnmutesNotify.swift | 4 +-
damus/TestData.swift | 1 -
damus/Types/DamusDuration.swift | 38 +++
damus/Util/Lists.swift | 70 ++--
damus/Util/Router.swift | 6 +-
damus/Views/DMChatView.swift | 2 +-
damus/Views/DirectMessagesView.swift | 2 +-
damus/Views/Events/EventMenu.swift | 28 +-
.../Events/EventMutingContainerView.swift | 37 ++-
damus/Views/Muting/AddMuteItemView.swift | 114 +++++++
damus/Views/Muting/MuteDurationMenu.swift | 40 +++
damus/Views/Muting/MutelistView.swift | 108 +++++--
damus/Views/Profile/ProfileView.swift | 11 +-
damus/Views/Reposts/RepostedEvent.swift | 4 +-
damus/Views/SearchHomeView.swift | 3 +-
damus/Views/SearchView.swift | 64 +++-
damus/Views/SideMenuView.swift | 2 +-
damus/Views/ThreadView.swift | 4 +-
damusTests/ListTests.swift | 42 +--
damusTests/LongPostTests.swift | 2 +-
damusTests/Mocking/MockDamusState.swift | 1 -
damusTests/Models/MutedItemTests.swift | 58 ++++
41 files changed, 1109 insertions(+), 293 deletions(-)
create mode 100644 damus/Models/MuteItem.swift
create mode 100644 damus/Types/DamusDuration.swift
create mode 100644 damus/Views/Muting/AddMuteItemView.swift
create mode 100644 damus/Views/Muting/MuteDurationMenu.swift
create mode 100644 damusTests/Models/MutedItemTests.swift
diff --git a/DamusNotificationService/NotificationExtensionState.swift b/DamusNotificationService/NotificationExtensionState.swift
index d6db01b4..69acc020 100644
--- a/DamusNotificationService/NotificationExtensionState.swift
+++ b/DamusNotificationService/NotificationExtensionState.swift
@@ -11,7 +11,6 @@ struct NotificationExtensionState: HeadlessDamusState {
let ndb: Ndb
let settings: UserSettingsStore
let contacts: Contacts
- let muted_threads: MutedThreadsManager
let keypair: Keypair
let profiles: Profiles
let zaps: Zaps
@@ -28,7 +27,6 @@ struct NotificationExtensionState: HeadlessDamusState {
self.settings = UserSettingsStore()
self.contacts = Contacts(our_pubkey: keypair.pubkey)
- self.muted_threads = MutedThreadsManager(keypair: keypair)
self.keypair = keypair
self.profiles = Profiles(ndb: ndb)
self.zaps = Zaps(our_pubkey: keypair.pubkey)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index c3347395..995142df 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -260,8 +260,8 @@
4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
- 4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; };
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */; };
+ 4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
@@ -421,10 +421,16 @@
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
+ B5172C692B4F49C600A661F3 /* AddMuteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5172C682B4F49C600A661F3 /* AddMuteItemView.swift */; };
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
+ B5A75C282B54614D007AFBC0 /* MuteDurationMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C272B54614D007AFBC0 /* MuteDurationMenu.swift */; };
+ B5A75C2A2B546D94007AFBC0 /* MutedItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MutedItemTests.swift */; };
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
+ B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
+ B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
+ B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; };
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -445,9 +451,6 @@
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
- D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
- D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
- D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; };
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; };
D74AAFC52B1538DF006CF0F4 /* NotificationExtensionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */; };
@@ -463,6 +466,9 @@
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
+ D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
+ D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
+ D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
@@ -499,7 +505,6 @@
D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
D7CB5D422B116F8900AD4105 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; };
- D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D442B116FE800AD4105 /* Contacts+.swift */; };
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; };
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
@@ -703,7 +708,7 @@
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
- 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
+ 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; usesTabs = 0; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -843,7 +848,7 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; };
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
- 4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferencedId.swift; sourceTree = "<group>"; };
+ 4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferencedId.swift; sourceTree = "<group>"; usesTabs = 0; };
4C2B10272A7B0F5C008AA43E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
4C2B7BF12A71B6540049DEE7 /* Id.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
@@ -885,16 +890,16 @@
4C363A9B282838B9006E126D /* EventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRef.swift; sourceTree = "<group>"; };
4C363A9D2828A822006E126D /* ReplyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyTests.swift; sourceTree = "<group>"; };
4C363A9F2828A8DD006E126D /* LikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeTests.swift; sourceTree = "<group>"; };
- 4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
+ 4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; usesTabs = 0; };
4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = "<group>"; };
4C3A1D3629637E0500558C0F /* PreviewCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCache.swift; sourceTree = "<group>"; };
- 4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; };
+ 4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; usesTabs = 0; };
4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowButtonView.swift; sourceTree = "<group>"; };
4C3AC7A02835A81400E1F516 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
4C3AC7A42836987600E1F516 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
- 4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeView.swift; sourceTree = "<group>"; };
+ 4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeView.swift; sourceTree = "<group>"; usesTabs = 0; };
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModel.swift; sourceTree = "<group>"; };
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrKind.swift; sourceTree = "<group>"; };
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.swift; sourceTree = "<group>"; };
@@ -1064,7 +1069,7 @@
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
4C5F9115283D855D0052CD1C /* EventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsModel.swift; sourceTree = "<group>"; };
4C5F9117283D88E40052CD1C /* FollowingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingModel.swift; sourceTree = "<group>"; };
- 4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
+ 4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; usesTabs = 0; };
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
@@ -1081,7 +1086,7 @@
4C75EFAC28049CFB0006080F /* PostButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostButton.swift; sourceTree = "<group>"; };
4C75EFAE28049D340006080F /* NostrFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilter.swift; sourceTree = "<group>"; };
4C75EFB028049D510006080F /* NostrResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrResponse.swift; sourceTree = "<group>"; };
- 4C75EFB228049D640006080F /* NostrEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEvent.swift; sourceTree = "<group>"; };
+ 4C75EFB228049D640006080F /* NostrEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEvent.swift; sourceTree = "<group>"; usesTabs = 0; };
4C75EFB428049D790006080F /* Relay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relay.swift; sourceTree = "<group>"; };
4C75EFB628049D990006080F /* RelayPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPool.swift; sourceTree = "<group>"; };
4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; };
@@ -1106,7 +1111,7 @@
4C7D09772A0B0CC900943473 /* WalletModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletModel.swift; sourceTree = "<group>"; };
4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectTests.swift; sourceTree = "<group>"; };
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
- 4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
+ 4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; usesTabs = 0; };
4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZappingNotify.swift; sourceTree = "<group>"; };
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachedWalletNotify.swift; sourceTree = "<group>"; };
4C8AE1182A0320BE00B944E6 /* Purple.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Purple.storekit; sourceTree = "<group>"; };
@@ -1135,8 +1140,8 @@
4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
- 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; };
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = "<group>"; };
+ 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
@@ -1200,7 +1205,7 @@
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
- 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
+ 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; usesTabs = 0; };
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
@@ -1222,7 +1227,7 @@
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRelayView.swift; sourceTree = "<group>"; };
4CE6DEE327F7A08100C66700 /*
damus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path =
damus.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CE6DEE627F7A08100C66700 /* damusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusApp.swift; sourceTree = "<group>"; };
- 4CE6DEE827F7A08100C66700 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+ 4CE6DEE827F7A08100C66700 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; usesTabs = 0; };
4CE6DEEA27F7A08200C66700 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4CE6DEED27F7A08200C66700 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
4CE6DEF327F7A08200C66700 /* damusTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = damusTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1249,12 +1254,12 @@
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = "<group>"; };
4CF0ABD32980996B00D66079 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = "<group>"; };
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>"; };
- 4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; };
+ 4CF0ABD72981980C00D66079 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = "<group>"; usesTabs = 0; };
+ 4CF0ABDB2981A19E00D66079 /* ListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = "<group>"; usesTabs = 0; };
+ 4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; usesTabs = 0; };
4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
4CF0ABE42981EE0C00D66079 /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = "<group>"; };
- 4CF0ABE6298444FC00D66079 /* EventMutingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMutingContainerView.swift; sourceTree = "<group>"; };
+ 4CF0ABE6298444FC00D66079 /* EventMutingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMutingContainerView.swift; sourceTree = "<group>"; usesTabs = 0; };
4CF0ABE829844AF100D66079 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
@@ -1308,10 +1313,15 @@
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = "<group>"; };
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = "<group>"; usesTabs = 0; };
+ B5172C682B4F49C600A661F3 /* AddMuteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMuteItemView.swift; sourceTree = "<group>"; usesTabs = 0; };
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = "<group>"; };
B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayAuthenticationDetail.swift; sourceTree = "<group>"; };
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
+ B5A75C272B54614D007AFBC0 /* MuteDurationMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteDurationMenu.swift; sourceTree = "<group>"; usesTabs = 0; };
+ B5A75C292B546D94007AFBC0 /* MutedItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
+ B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = "<group>"; usesTabs = 0; };
+ B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = "<group>"; usesTabs = 0; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -1332,15 +1342,15 @@
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
- D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
- D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
- D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExtensionState.swift; sourceTree = "<group>"; };
D74AAFCB2B155D07006CF0F4 /* MakeZapRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeZapRequest.swift; sourceTree = "<group>"; };
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
+ D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
+ D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
+ D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
@@ -1355,8 +1365,8 @@
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
- D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = "<group>"; };
- D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = "<group>"; };
+ D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = "<group>"; usesTabs = 0; };
+ D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = "<group>"; usesTabs = 0; };
D7CB5D4A2B11721600AD4105 /* ZapType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapType.swift; sourceTree = "<group>"; };
D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewEventsBits.swift; sourceTree = "<group>"; };
D7CB5D502B1174D100AD4105 /* FriendFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendFilter.swift; sourceTree = "<group>"; };
@@ -1577,6 +1587,7 @@
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */,
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */,
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
+ B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -2279,6 +2290,7 @@
4CC14FED2A73FCBB007AEB17 /* Ids */,
7527271D2A93FF0100214108 /* Block.swift */,
D798D21D2B0858BB00234419 /* MigratedTypes.swift */,
+ B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */,
);
path = Types;
sourceTree = "<group>";
@@ -2532,6 +2544,8 @@
isa = PBXGroup;
children = (
4CF0ABE02981A83900D66079 /* MutelistView.swift */,
+ B5172C682B4F49C600A661F3 /* AddMuteItemView.swift */,
+ B5A75C272B54614D007AFBC0 /* MuteDurationMenu.swift */,
);
path = Muting;
sourceTree = "<group>";
@@ -2659,6 +2673,7 @@
children = (
F944F56D29EA9CCC0067B3BF /* DamusParseContentTests.swift */,
75AD872A2AA23A460085EF2C /* Block+Tests.swift */,
+ B5A75C292B546D94007AFBC0 /* MutedItemTests.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -2921,6 +2936,7 @@
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */,
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
+ B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */,
4C32B9522A9AD44700DC3548 /* Message.swift in Sources */,
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
@@ -3029,6 +3045,7 @@
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */,
+ B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
@@ -3260,6 +3277,7 @@
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
+ B5A75C282B54614D007AFBC0 /* MuteDurationMenu.swift in Sources */,
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
@@ -3282,6 +3300,7 @@
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
+ B5172C692B4F49C600A661F3 /* AddMuteItemView.swift in Sources */,
F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -3360,6 +3379,7 @@
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
+ B5A75C2A2B546D94007AFBC0 /* MutedItemTests.swift in Sources */,
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
@@ -3481,7 +3501,6 @@
D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */,
D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */,
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
- D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */,
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
@@ -3520,6 +3539,7 @@
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */,
D7CE1B412B0BE719002EDAD4 /* FlatBuffersUtils.swift in Sources */,
D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */,
+ B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */,
D798D2262B085C4200234419 /* Bech32.swift in Sources */,
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */,
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
index e7e0a61c..4ee707b5 100644
--- a/damus/ContentView.swift
+++ b/damus/ContentView.swift
@@ -69,7 +69,7 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil
@State var damus_state: DamusState!
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
- @State var muting: Pubkey? = nil
+ @State var muting: MuteItem? = nil
@State var confirm_mute: Bool = false
@State var hide_bar: Bool = false
@State var user_muted_confirm: Bool = false
@@ -361,8 +361,8 @@ struct ContentView: View {
.onReceive(handle_notify(.report)) { target in
self.active_sheet = .report(target)
}
- .onReceive(handle_notify(.mute)) { pubkey in
- self.muting = pubkey
+ .onReceive(handle_notify(.mute)) { mute_item in
+ self.muting = mute_item
self.confirm_mute = true
}
.onReceive(handle_notify(.attached_wallet)) { nwc in
@@ -527,7 +527,7 @@ struct ContentView: View {
user_muted_confirm = false
}
}, message: {
- if let pubkey = self.muting {
+ if case let .user(pubkey, _) = self.muting {
let profile_txn = damus_state!.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
@@ -545,13 +545,13 @@ struct ContentView: View {
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
guard let ds = damus_state,
let keypair = ds.keypair.to_full(),
- let pubkey = muting,
- let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
+ let muting,
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting)
else {
return
}
- damus_state?.contacts.set_mutelist(mutelist)
+ ds.contacts.set_mutelist(mutelist)
ds.postbox.send(mutelist)
confirm_overwrite_mutelist = false
@@ -574,21 +574,21 @@ struct ContentView: View {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full(),
- let pubkey = muting
+ let muting
else {
return
}
- guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
+ guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: muting) else {
return
}
- damus_state?.contacts.set_mutelist(ev)
+ ds.contacts.set_mutelist(ev)
ds.postbox.send(ev)
}
}
}, message: {
- if let pubkey = muting {
+ if case let .user(pubkey, _) = muting {
let profile_txn = damus_state?.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
@@ -675,7 +675,6 @@ struct ContentView: View {
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
- muted_threads: MutedThreadsManager(keypair: keypair),
wallet: WalletModel(settings: settings),
nav: self.navigationCoordinator,
music: MusicController(onChange: music_changed),
@@ -1067,7 +1066,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
result(.event(ev))
}
case .hashtag(let ht):
- result(.filter(.filter_hashtag([ht.string()])))
+ result(.filter(.filter_hashtag([ht.hashtag])))
case .param, .quote:
// doesn't really make sense here
break
diff --git a/damus/Models/Contacts+.swift b/damus/Models/Contacts+.swift
index 19d6f6d4..8e624df2 100644
--- a/damus/Models/Contacts+.swift
+++ b/damus/Models/Contacts+.swift
@@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
return contacts.references.contains { ref in
switch (ref, follow) {
case let (.hashtag(ht), .hashtag(follow_ht)):
- return ht.string() == follow_ht
+ return ht.hashtag == follow_ht
case let (.pubkey(pk), .pubkey(follow_pk)):
return pk == follow_pk
case (.hashtag, .pubkey), (.pubkey, .hashtag),
diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift
index 5829ed9c..994e36b7 100644
--- a/damus/Models/Contacts.swift
+++ b/damus/Models/Contacts.swift
@@ -7,13 +7,12 @@
import Foundation
-
class Contacts {
private var friends: Set<Pubkey> = Set()
private var friend_of_friends: Set<Pubkey> = Set()
/// Tracks which friends are friends of a given pubkey.
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
- private var muted: Set<Pubkey> = Set()
+ private var muted: Set<MuteItem> = Set()
let our_pubkey: Pubkey
var event: NostrEvent?
@@ -23,20 +22,20 @@ class Contacts {
self.our_pubkey = our_pubkey
}
- func is_muted(_ pk: Pubkey) -> Bool {
- return muted.contains(pk)
+ func is_muted(_ item: MuteItem) -> Bool {
+ return muted.contains(item)
}
-
+
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
- let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
- let new = Set(ev.referenced_pubkeys)
+ 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<Pubkey>()
- var new_unmutes = Set<Pubkey>()
+ var new_mutes = Set<MuteItem>()
+ var new_unmutes = Set<MuteItem>()
for d in diff {
if new.contains(d) {
@@ -47,7 +46,7 @@ class Contacts {
}
// TODO: set local mutelist here
- self.muted = Set(ev.referenced_pubkeys)
+ self.muted = ev.mute_list ?? Set<MuteItem>()
if new_mutes.count > 0 {
notify(.new_mutes(new_mutes))
diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift
index da3c6ba5..c876f3b6 100644
--- a/damus/Models/ContentFilters.swift
+++ b/damus/Models/ContentFilters.swift
@@ -33,7 +33,7 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
guard ev.known_kind == .boost else { return true }
// This needs to use cached because it can be way too slow otherwise
guard let inner_ev = ev.get_cached_inner_event(cache: damus_state.events) else { return true }
- return should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: inner_ev)
+ return should_show_event(contacts: damus_state.contacts, ev: inner_ev)
}
}
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
index 14d3fe91..502abea2 100644
--- a/damus/Models/DamusState.swift
+++ b/damus/Models/DamusState.swift
@@ -28,7 +28,6 @@ struct DamusState: HeadlessDamusState {
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
- let muted_threads: MutedThreadsManager
let wallet: WalletModel
let nav: NavigationCoordinator
let music: MusicController?
@@ -36,7 +35,7 @@ struct DamusState: HeadlessDamusState {
let ndb: Ndb
var purple: DamusPurple
- init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
+ init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
self.pool = pool
self.keypair = keypair
self.likes = likes
@@ -56,7 +55,6 @@ struct DamusState: HeadlessDamusState {
self.postbox = postbox
self.bootstrap_relays = bootstrap_relays
self.replies = replies
- self.muted_threads = muted_threads
self.wallet = wallet
self.nav = nav
self.music = music
@@ -118,7 +116,6 @@ struct DamusState: HeadlessDamusState {
postbox: PostBox(pool: RelayPool(ndb: .empty)),
bootstrap_relays: [],
replies: ReplyCounter(our_pubkey: empty_pub),
- muted_threads: MutedThreadsManager(keypair: kp),
wallet: WalletModel(settings: UserSettingsStore()),
nav: NavigationCoordinator(),
music: nil,
diff --git a/damus/Models/HeadlessDamusState.swift b/damus/Models/HeadlessDamusState.swift
index 15f785f0..dcb9d032 100644
--- a/damus/Models/HeadlessDamusState.swift
+++ b/damus/Models/HeadlessDamusState.swift
@@ -15,7 +15,6 @@ protocol HeadlessDamusState {
var ndb: Ndb { get }
var settings: UserSettingsStore { get }
var contacts: Contacts { get }
- var muted_threads: MutedThreadsManager { get }
var keypair: Keypair { get }
var profiles: Profiles { get }
var zaps: Zaps { get }
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
index 4210ce3e..c32d25c8 100644
--- a/damus/Models/HomeModel.swift
+++ b/damus/Models/HomeModel.swift
@@ -157,8 +157,10 @@ class HomeModel {
case .metadata:
// profile metadata processing is handled by nostrdb
break
- case .list:
- handle_list_event(ev)
+ case .mute_list:
+ handle_mute_list_event(ev)
+ case .list_deprecated:
+ handle_old_list_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
@@ -242,7 +244,7 @@ class HomeModel {
process_zap_event(state: damus_state, ev: ev) { zapres in
guard case .done(let zap) = zapres,
zap.target.pubkey == self.damus_state.keypair.pubkey,
- should_show_event(keypair: self.damus_state.keypair, hellthreads: self.damus_state.muted_threads, contacts: self.damus_state.contacts, ev: zap.request.ev) else {
+ should_show_event(contacts: self.damus_state.contacts, ev: zap.request.ev) else {
return
}
@@ -276,11 +278,11 @@ class HomeModel {
func filter_events() {
events.filter { ev in
- !damus_state.contacts.is_muted(ev.pubkey)
+ !damus_state.contacts.is_muted(.user(ev.pubkey, nil))
}
self.dms.dms = dms.dms.filter { ev in
- !damus_state.contacts.is_muted(ev.pubkey)
+ !damus_state.contacts.is_muted(.user(ev.pubkey, nil))
}
notifications.filter { ev in
@@ -288,7 +290,8 @@ class HomeModel {
return false
}
- return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair)
+ let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil
+ return !event_muted
}
}
@@ -461,10 +464,13 @@ class HomeModel {
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
our_contacts_filter.authors = [damus_state.pubkey]
- var our_blocklist_filter = NostrFilter(kinds: [.list])
- our_blocklist_filter.parameter = ["mute"]
+ var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated])
+ our_old_blocklist_filter.parameter = ["mute"]
+ our_old_blocklist_filter.authors = [damus_state.pubkey]
+
+ var our_blocklist_filter = NostrFilter(kinds: [.mute_list])
our_blocklist_filter.authors = [damus_state.pubkey]
-
+
var dms_filter = NostrFilter(kinds: [.dm])
var our_dms_filter = NostrFilter(kinds: [.dm])
@@ -488,7 +494,7 @@ class HomeModel {
notifications_filter.limit = 500
var notifications_filters = [notifications_filter]
- var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
+ var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = get_last_of_kind(relay_id: relay_id)
@@ -557,12 +563,31 @@ class HomeModel {
pool.send(.subscribe(sub), to: relay_ids)
}
- func handle_list_event(_ ev: NostrEvent) {
+ func handle_mute_list_event(_ ev: NostrEvent) {
+ // we only care about our mutelist
+ guard ev.pubkey == damus_state.pubkey else {
+ return
+ }
+
+ // we only care about the most recent mutelist
+ if let mutelist = damus_state.contacts.mutelist {
+ if ev.created_at <= mutelist.created_at {
+ return
+ }
+ }
+
+ damus_state.contacts.set_mutelist(ev)
+
+ migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
+ }
+
+ func handle_old_list_event(_ ev: NostrEvent) {
// we only care about our lists
guard ev.pubkey == damus_state.pubkey else {
return
}
+ // we only care about the most recent mutelist
if let mutelist = damus_state.contacts.mutelist {
if ev.created_at <= mutelist.created_at {
return
@@ -574,6 +599,8 @@ class HomeModel {
}
damus_state.contacts.set_mutelist(ev)
+
+ migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
}
func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? {
@@ -589,7 +616,7 @@ class HomeModel {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey,
event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey),
- should_show_event(keypair: self.damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
+ should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -627,7 +654,7 @@ class HomeModel {
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
- guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
+ guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -656,7 +683,7 @@ class HomeModel {
}
func handle_dm(_ ev: NostrEvent) {
- guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
+ guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -1063,19 +1090,14 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
return should_show_event(
- keypair: damus_state.keypair,
- hellthreads: damus_state.muted_threads,
contacts: damus_state.contacts,
ev: event
)
}
-func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool {
- if contacts.is_muted(ev.pubkey) {
- return false
- }
-
- if hellthreads.isMutedThread(ev, keypair: keypair) {
+func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
+ let event_muted = contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil
+ if event_muted {
return false
}
@@ -1136,3 +1158,240 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
}
}
}
+//
+//func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String {
+//
+// let prefix_len = 300
+// let artifacts = cache.get_cache_data(
ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, keypair: keypair)
+//
+// // special case for longform events
+// if ev.known_kind == .longform {
+// let longform = LongformEvent(event: ev)
+// return longform.title ?? longform.summary ?? "Longform Event"
+// }
+//
+// switch artifacts {
+// case .longform:
+// // we should never hit this until we have more note types built out of parts
+// // since we handle this case above in known_kind == .longform
+// return String(ev.content.prefix(prefix_len))
+//
+// case .separated(let artifacts):
+// return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len))
+// }
+//}
+//
+//func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
+// guard let type = ev.known_kind else {
+// return
+// }
+//
+// if damus_state.settings.notification_only_from_following,
+// damus_state.contacts.follow_state(ev.pubkey) != .follows
+// {
+// return
+// }
+//
+// // Don't show notifications from muted threads.
+// let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil
+// if event_muted {
+// return
+// }
+//
+// // Don't show notifications for old events
+// guard ev.age < HomeModel.event_max_age_for_notification else {
+// return
+// }
+//
+// if type == .text, damus_state.settings.mention_notification {
+// let blocks = ev.blocks(damus_state.keypair).blocks
+// for case .mention(let mention) in blocks {
+// guard case .pubkey(let pk) = mention.ref, pk == damus_state.keypair.pubkey else {
+// continue
+// }
+// let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
+// let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
+// create_local_notification(profiles: damus_state.profiles, notify: notify )
+// }
+// } else if type == .boost,
+// damus_state.settings.repost_notification,
+// let inner_ev = ev.get_inner_event(cache: damus_state.events)
+// {
+// let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
+// let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
+// create_local_notification(profiles: damus_state.profiles, notify: notify)
+// } else if type == .like,
+// damus_state.settings.like_notification,
+// let evid = ev.referenced_ids.last,
+// let liked_event = damus_state.events.lookup(evid)
+// {
+// let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, keypair: damus_state.keypair)
+// let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
+// create_local_notification(profiles: damus_state.profiles, notify: notify)
+// }
+//
+//}
+//
+//func create_local_notification(profiles: Profiles, notify: LocalNotification) {
+// let content = UNMutableNotificationContent()
+// var title = ""
+// var identifier = ""
+//
+// let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
+//
+// switch notify.type {
+// case .mention:
+// title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
+// identifier = "myMentionNotification"
+// case .repost:
+// title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
+// identifier = "myBoostNotification"
+// case .like:
+// title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
+// identifier = "myLikeNotification"
+// case .dm:
+// title = displayName
+// identifier = "myDMNotification"
+// case .zap, .profile_zap:
+// // not handled here
+// break
+// }
+// content.title = title
+// content.body = notify.content
+// content.sound = UNNotificationSound.default
+// content.userInfo = notify.to_lossy().to_user_info()
+//
+// let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+//
+// let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
+//
+// UNUserNotificationCenter.current().add(request) { error in
+// if let error = error {
+// print("Error: \(error)")
+// } else {
+// print("Local notification scheduled")
+// }
+// }
+//}
+//
+//
+//enum ProcessZapResult {
+// case already_processed(Zap)
+// case done(Zap)
+// case failed
+//}
+//
+//extension Sequence {
+// func just_one() -> Element? {
+// var got_one = false
+// var the_x: Element? = nil
+// for x in self {
+// guard !got_one else {
+// return nil
+// }
+// the_x = x
+// got_one = true
+// }
+// return the_x
+// }
+//}
+//
+//// securely get the zap target's pubkey. this can be faked so we need to be
+//// careful
+//func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? {
+// let etags = Array(ev.referenced_ids)
+//
+// guard let etag = etags.first else {
+// // no etags, ptag-only case
+//
+// guard let a = ev.referenced_pubkeys.just_one() else {
+// return nil
+// }
+//
+// // TODO: just return data here
+// return a
+// }
+//
+// // we have an e-tag
+//
+// // ensure that there is only 1 etag to stop fake note zap attacks
+// guard etags.count == 1 else {
+// return nil
+// }
+//
+// // we can't trust the p tag on note zaps because they can be faked
+// guard let pk = events.lookup(etag)?.pubkey else {
+// // We don't have the event in cache so we can't check the pubkey.
+//
+// // We could return this as an invalid zap but that wouldn't be correct
+// // all of the time, and may reject valid zaps. What we need is a new
+// // unvalidated zap state, but for now we simply leak a bit of correctness...
+//
+// return ev.referenced_pubkeys.just_one()
+// }
+//
+// return pk
+//}
+//
+//@MainActor
+//func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
+// // These are zap notifications
+// guard let ptag = get_zap_target_pubkey(ev: ev, events: damus_state.events) else {
+// completion(.failed)
+// return
+// }
+//
+// // just return the zap if we already have it
+// if let zap = damus_state.zaps.zaps[
ev.id], case .zap(let z) = zap {
+// completion(.already_processed(z))
+// return
+// }
+//
+// if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
+// guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else {
+// completion(.failed)
+// return
+// }
+// damus_state.add_zap(zap: .zap(zap))
+// completion(.done(zap))
+// return
+// }
+//
+// guard let lnurl = damus_state.profiles.lookup_with_timestamp(ptag)
+// .map({ pr in pr?.lnurl }).value else {
+// completion(.failed)
+// return
+// }
+//
+// Task { [lnurl] in
+// guard let zapper = await fetch_zapper_from_lnurl(lnurls: damus_state.lnurls, pubkey: ptag, lnurl: lnurl) else {
+// completion(.failed)
+// return
+// }
+//
+// DispatchQueue.main.async {
+// damus_state.profiles.profile_data(ptag).zapper = zapper
+// guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
+// completion(.failed)
+// return
+// }
+// damus_state.add_zap(zap: .zap(zap))
+// completion(.done(zap))
+// }
+// }
+//
+//
+//}
+//
+//fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? {
+// let our_keypair = damus_state.keypair
+//
+// guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
+// return nil
+// }
+//
+// damus_state.add_zap(zap: .zap(zap))
+//
+// return zap
+//}
+//
diff --git a/damus/Models/MuteItem.swift b/damus/Models/MuteItem.swift
new file mode 100644
index 00000000..dcc83e5a
--- /dev/null
+++ b/damus/Models/MuteItem.swift
@@ -0,0 +1,208 @@
+//
+// 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, let rhs_expiration_date)):
+ return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
+ case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
+ return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
+ case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
+ return lhs_word == rhs_word && !rhs.is_expired()
+ case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
+ 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
+ }
+ }
+}
+
+extension Collection where Element == MuteItem {
+ /// 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 event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
+ return self.first { muted_item in
+ switch muted_item {
+ case .user(let pubkey, let expiration_date):
+ return pubkey == ev.pubkey && !muted_item.is_expired()
+ case .hashtag(let hashtag, let expiration_date):
+ return ev.referenced_hashtags.contains(hashtag) && !muted_item.is_expired()
+ case .word(let word, let expiration_date):
+ return ev.content.lowercased().contains(word.lowercased()) && !muted_item.is_expired()
+ case .thread(let note_id, let expiration_date):
+ return ev.referenced_ids.contains(note_id) && !muted_item.is_expired()
+ }
+ }
+ }
+
+ var users: [Pubkey] {
+ return self.compactMap { muted_item in
+ if case .user(let pubkey, _) = muted_item,
+ !muted_item.is_expired() {
+ return pubkey
+ } else {
+ return nil
+ }
+ }
+ }
+ var hashtags: [Hashtag] {
+ return self.compactMap { muted_item in
+ if case .hashtag(let hashtag, _) = muted_item,
+ !muted_item.is_expired() {
+ return hashtag
+ } else {
+ return nil
+ }
+ }
+ }
+ var words: [String] {
+ return self.compactMap { muted_item in
+ if case .word(let str, _) = muted_item,
+ !muted_item.is_expired() {
+ return str
+ } else {
+ return nil
+ }
+ }
+ }
+ var threads: [NoteId] {
+ return self.compactMap { muted_item in
+ if case .thread(let note_id, _) = muted_item,
+ !muted_item.is_expired() {
+ return note_id
+ } else {
+ return nil
+ }
+ }
+ }
+}
diff --git a/damus/Models/MutedThreadsManager.swift b/damus/Models/MutedThreadsManager.swift
index 80903e4a..20c1903f 100644
--- a/damus/Models/MutedThreadsManager.swift
+++ b/damus/Models/MutedThreadsManager.swift
@@ -11,7 +11,7 @@ fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
pk_setting_key(pubkey, key: "muted_threads")
}
-func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
+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
@@ -20,56 +20,20 @@ func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
}
}
-func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool {
- let uniqueMutedThreads = Array(Set(value))
-
- if uniqueMutedThreads != currentValue {
- let ids = uniqueMutedThreads.map { note_id in return note_id.hex() }
- UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey))
- return true
- }
-
- return false
-}
-
-class MutedThreadsManager: ObservableObject {
-
- private let keypair: Keypair
-
- private var _mutedThreadsSet: Set<NoteId>
- private var _mutedThreads: [NoteId]
- var mutedThreads: [NoteId] {
- get {
- return _mutedThreads
- }
- set {
- if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
- self._mutedThreads = newValue
- self.objectWillChange.send()
- }
- }
- }
-
- init(keypair: Keypair) {
- self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
- self._mutedThreadsSet = Set(_mutedThreads)
- self.keypair = keypair
- }
-
- func isMutedThread(_ ev: NostrEvent, keypair: Keypair) -> Bool {
- return _mutedThreadsSet.contains(ev.thread_id(keypair: keypair))
- }
-
- func updateMutedThread(_ ev: NostrEvent) {
- let threadId = ev.thread_id(keypair: keypair)
- if isMutedThread(ev, keypair: keypair) {
- mutedThreads = mutedThreads.filter { $0 != threadId }
- _mutedThreadsSet.remove(threadId)
- notify(.unmute_thread(ev))
- } else {
- mutedThreads.append(threadId)
- _mutedThreadsSet.insert(threadId)
- notify(.mute_thread(ev))
- }
- }
+// 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.contacts.mutelist
+ 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.contacts.set_mutelist(new_mutelist_event)
+ damus_state.postbox.send(new_mutelist_event)
+ // Set existing muted threads to an empty array
+ UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
}
diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift
index 6a832362..5718f56c 100644
--- a/damus/Models/NotificationsManager.swift
+++ b/damus/Models/NotificationsManager.swift
@@ -36,16 +36,11 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
return false
}
- // Don't show notifications from muted threads.
- if state.muted_threads.isMutedThread(ev, keypair: state.keypair) {
+ // Don't show notifications that match mute list.
+ if state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil {
return false
}
-
- // Don't show notifications from muted users
- if state.contacts.is_muted(ev.pubkey) {
- return false
- }
-
+
// Don't show notifications for old events
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
return false
diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift
index 84ae5911..63bcc810 100644
--- a/damus/Models/SearchHomeModel.swift
+++ b/damus/Models/SearchHomeModel.swift
@@ -35,7 +35,7 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
- events.filter { should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: $0) }
+ events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
self.objectWillChange.send()
}
@@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
- if ev.is_textlike && should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair)
+ if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair)
{
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
return
diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift
index b61231af..1d107fc3 100644
--- a/damus/Models/SearchModel.swift
+++ b/damus/Models/SearchModel.swift
@@ -28,7 +28,7 @@ class SearchModel: ObservableObject {
func filter_muted() {
self.events.filter {
- should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: $0)
+ should_show_event(contacts: state.contacts, ev: $0)
}
self.objectWillChange.send()
}
@@ -57,7 +57,7 @@ class SearchModel: ObservableObject {
return
}
- guard should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: ev) else {
+ guard should_show_event(contacts: state.contacts, ev: ev) else {
return
}
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
index 98697ce1..72e4122b 100644
--- a/damus/Nostr/NostrEvent.swift
+++ b/damus/Nostr/NostrEvent.swift
@@ -790,3 +790,17 @@ func to_reaction_emoji(ev: NostrEvent) -> String? {
}
}
+extension NostrEvent {
+ /// The mutelist for a given event
+ ///
+ /// If the event is not a mutelist it will return `nil`.
+ var mute_list: Set<MuteItem>? {
+ if self.kind == NostrKind.list_deprecated.rawValue && self.referenced_params.contains(where: { p in p.param.matches_str("mute") }) {
+ return Set(self.referenced_pubkeys.map { MuteItem.user($0, nil) })
+ } else if self.kind == NostrKind.mute_list.rawValue {
+ return Set(self.tags.strings().compactMap { MuteItem.init($0) })
+ } else {
+ return nil
+ }
+ }
+}
diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift
index dd645946..18578d8d 100644
--- a/damus/Nostr/NostrKind.swift
+++ b/damus/Nostr/NostrKind.swift
@@ -17,7 +17,8 @@ enum NostrKind: UInt32, Codable {
case boost = 6
case like = 7
case chat = 42
- case list = 30000
+ case mute_list = 10000
+ case list_deprecated = 30000
case longform = 30023
case zap = 9735
case zap_request = 9734
diff --git a/damus/Nostr/ReferencedId.swift b/damus/Nostr/ReferencedId.swift
index f7731514..67e79ac4 100644
--- a/damus/Nostr/ReferencedId.swift
+++ b/damus/Nostr/ReferencedId.swift
@@ -119,7 +119,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case event(NoteId)
case pubkey(Pubkey)
case quote(QuoteId)
- case hashtag(TagElem)
+ case hashtag(Hashtag)
case param(TagElem)
var key: RefKey {
@@ -153,7 +153,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .event(let noteId): return noteId.hex()
case .pubkey(let pubkey): return pubkey.hex()
case .quote(let quote): return quote.hex()
- case .hashtag(let string): return string.string()
+ case .hashtag(let string): return string.hashtag
case .param(let string): return string.string()
}
}
@@ -172,7 +172,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .e: return
t1.id().map({ .event(NoteId($0)) })
case .p: return
t1.id().map({ .pubkey(Pubkey($0)) })
case .q: return
t1.id().map({ .quote(QuoteId($0)) })
- case .t: return .hashtag(t1)
+ case .t: return .hashtag(Hashtag(hashtag: t1.string()))
case .d: return .param(t1)
}
}
diff --git a/damus/Notify/MuteNotify.swift b/damus/Notify/MuteNotify.swift
index db15e519..77263fbb 100644
--- a/damus/Notify/MuteNotify.swift
+++ b/damus/Notify/MuteNotify.swift
@@ -8,8 +8,8 @@
import Foundation
struct MuteNotify: Notify {
- typealias Payload = Pubkey
- var payload: Payload
+ typealias Payload = MuteItem
+ var payload: MuteItem
}
extension NotifyHandler {
@@ -19,7 +19,7 @@ extension NotifyHandler {
}
extension Notifications {
- static func mute(_ target: Pubkey) -> Notifications<MuteNotify> {
+ static func mute(_ target: MuteItem) -> Notifications<MuteNotify> {
.init(.init(payload: target))
}
}
diff --git a/damus/Notify/NewMutesNotify.swift b/damus/Notify/NewMutesNotify.swift
index bd3ae75d..201ae4f3 100644
--- a/damus/Notify/NewMutesNotify.swift
+++ b/damus/Notify/NewMutesNotify.swift
@@ -8,7 +8,7 @@
import Foundation
struct NewMutesNotify: Notify {
- typealias Payload = Set<Pubkey>
+ typealias Payload = Set<MuteItem>
var payload: Payload
}
@@ -19,7 +19,7 @@ extension NotifyHandler {
}
extension Notifications {
- static func new_mutes(_ pubkeys: Set<Pubkey>) -> Notifications<NewMutesNotify> {
+ static func new_mutes(_ pubkeys: Set<MuteItem>) -> Notifications<NewMutesNotify> {
.init(.init(payload: pubkeys))
}
}
diff --git a/damus/Notify/NewUnmutesNotify.swift b/damus/Notify/NewUnmutesNotify.swift
index d5bf0f6c..a5c85ae1 100644
--- a/damus/Notify/NewUnmutesNotify.swift
+++ b/damus/Notify/NewUnmutesNotify.swift
@@ -8,7 +8,7 @@
import Foundation
struct NewUnmutesNotify: Notify {
- typealias Payload = Set<Pubkey>
+ typealias Payload = Set<MuteItem>
var payload: Payload
}
@@ -19,7 +19,7 @@ extension NotifyHandler {
}
extension Notifications {
- static func new_unmutes(_ pubkeys: Set<Pubkey>) -> Notifications<NewUnmutesNotify> {
+ static func new_unmutes(_ pubkeys: Set<MuteItem>) -> Notifications<NewUnmutesNotify> {
.init(.init(payload: pubkeys))
}
}
diff --git a/damus/TestData.swift b/damus/TestData.swift
index 5b0a3b48..f9515bb5 100644
--- a/damus/TestData.swift
+++ b/damus/TestData.swift
@@ -87,7 +87,6 @@ var test_damus_state: DamusState = ({
postbox: .init(pool: pool),
bootstrap_relays: .init(),
replies: .init(our_pubkey: our_pubkey),
- muted_threads: .init(keypair: test_keypair),
wallet: .init(settings: settings),
nav: .init(),
music: .init(onChange: {_ in }),
diff --git a/damus/Types/DamusDuration.swift b/damus/Types/DamusDuration.swift
new file mode 100644
index 00000000..fa499b41
--- /dev/null
+++ b/damus/Types/DamusDuration.swift
@@ -0,0 +1,38 @@
+//
+// DamusDuration.swift
+// damus
+//
+// Created by Charlie Fish on 1/13/24.
+//
+
+import Foundation
+
+enum DamusDuration: CaseIterable {
+ case day
+ case week
+ case month
+
+ var title: String {
+ switch self {
+ case .day:
+ return NSLocalizedString("24 hours", comment: "A duration of 24 hours/1 day to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.")
+ case .week:
+ return NSLocalizedString("1 week", comment: "A duration of 1 week to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.")
+ case .month:
+ return NSLocalizedString("1 month", comment: "A duration of 1 month to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.")
+ }
+ }
+
+ var date_from_now: Date? {
+ let current_date = Date()
+
+ switch self {
+ case .day:
+ return Calendar.current.date(byAdding: .day, value: 1, to: current_date)
+ case .week:
+ return Calendar.current.date(byAdding: .day, value: 7, to: current_date)
+ case .month:
+ return Calendar.current.date(byAdding: .month, value: 1, to: current_date)
+ }
+ }
+}
diff --git a/damus/Util/Lists.swift b/damus/Util/Lists.swift
index 9c67221e..303e6fde 100644
--- a/damus/Util/Lists.swift
+++ b/damus/Util/Lists.swift
@@ -7,64 +7,30 @@
import Foundation
-func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId) -> NostrEvent? {
- return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p")
+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 remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? {
- return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove)
+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 create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId, list_name: String, list_type: String) -> NostrEvent? {
- if let prev = mprev,
- prev.pubkey == keypair.pubkey,
- matches_list_name(tags: prev.tags, name: list_name)
- {
- return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add)
- }
-
- let tags = [["d", list_name], [list_type, to_add.description]]
- return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 30000, tags: tags)
-}
-
-func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? {
- var removed = false
-
- let tags = prev.tags.reduce(into: [[String]](), { acc, tag in
- if let ref_id = RefId.from_tag(tag: tag), ref_id == to_remove {
- removed = true
- return
- }
- acc.append(tag.strings())
- })
-
- guard removed else {
- return nil
- }
-
- return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags)
+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 add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: RefId) -> NostrEvent? {
- for tag in prev.tags {
- // we are already muting this user
- if let ref = RefId.from_tag(tag: tag), to_add == ref {
- return nil
- }
- }
-
- var tags = prev.tags.strings()
- tags.append(to_add.tag)
+func toggle_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_toggle: MuteItem) -> NostrEvent? {
+ let existing_muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>())
- return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags)
-}
-
-func matches_list_name(tags: Tags, name: String) -> Bool {
- for tag in tags {
- if tag.count >= 2 && tag[0].matches_char("d") {
- return tag[1].matches_str(name)
- }
+ 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)
}
-
- return false
}
diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift
index 292f90bd..fbcaf801 100644
--- a/damus/Util/Router.swift
+++ b/damus/Util/Router.swift
@@ -14,7 +14,7 @@ enum Route: Hashable {
case Relay(relay: String, showActionButtons: Binding<Bool>)
case RelayDetail(relay: String, metadata: RelayMetadata?)
case Following(following: FollowingModel)
- case MuteList(users: [Pubkey])
+ case MuteList(mutelist_items: Set<MuteItem>)
case RelayConfig
case Script(script: ScriptModel)
case Bookmarks
@@ -58,8 +58,8 @@ enum Route: Hashable {
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
case .Following(let following):
FollowingView(damus_state: damusState, following: following)
- case .MuteList(let users):
- MutelistView(damus_state: damusState, users: users)
+ case .MuteList(let mutelist_items):
+ MutelistView(damus_state: damusState, mutelist_items: mutelist_items)
case .RelayConfig:
RelayConfigView(state: damusState)
case .Bookmarks:
diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift
index 217f022e..0e9484b5 100644
--- a/damus/Views/DMChatView.swift
+++ b/damus/Views/DMChatView.swift
@@ -22,7 +22,7 @@ struct DMChatView: View, KeyboardReadable {
LazyVStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)), 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)}
+ .contextMenu{MenuItems(damus_state: damus_state, event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, settings: damus_state.settings)}
}
EndBlock(height: 1)
}
diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift
index 2ff3da74..6d3cfacc 100644
--- a/damus/Views/DirectMessagesView.swift
+++ b/damus/Views/DirectMessagesView.swift
@@ -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.contacts.is_muted(.user(dm.pubkey, nil))
})
}
diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift
index 183824f5..28441696 100644
--- a/damus/Views/Events/EventMenu.swift
+++ b/damus/Views/Events/EventMenu.swift
@@ -8,19 +8,19 @@
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
@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)
}
@@ -32,7 +32,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)
+ MenuItems(damus_state: damus_state, event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, settings: settings)
} label: {
Color.clear
}
@@ -47,26 +47,26 @@ 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
@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) {
+ init(damus_state: DamusState, event: NostrEvent, keypair: Keypair, target_pubkey: Pubkey, bookmarks: BookmarksManager, settings: UserSettingsStore) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
- let muted_thread = muted_threads.isMutedThread(event, keypair: keypair)
+ let muted_thread = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil)
self._isMutedThread = State(initialValue: muted_thread)
+ self.damus_state = damus_state
self.bookmarks = bookmarks
- self.muted_threads = muted_threads
self.event = event
self.keypair = keypair
self.target_pubkey = target_pubkey
@@ -112,9 +112,13 @@ struct MenuItems: View {
}
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.keypair.to_full(),
+ let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.contacts.mutelist, to_toggle: .thread(event.thread_id(keypair: keypair), duration?.date_from_now)) {
+ damus_state.contacts.set_mutelist(new_mutelist_ev)
+ damus_state.postbox.send(new_mutelist_ev)
+ }
+ let muted = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil)
isMutedThread = muted
} label: {
let imageName = isMutedThread ? "mute" : "mute"
@@ -138,8 +142,8 @@ struct MenuItems: View {
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")
}
diff --git a/damus/Views/Events/EventMutingContainerView.swift b/damus/Views/Events/EventMutingContainerView.swift
index e8b7cb41..c030be24 100644
--- a/damus/Views/Events/EventMutingContainerView.swift
+++ b/damus/Views/Events/EventMutingContainerView.swift
@@ -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(contacts: damus_state.contacts, 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(contacts: damus_state.contacts, 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 = mutes.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 unmutes.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()
}
}
diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift
new file mode 100644
index 00000000..d975894c
--- /dev/null
+++ b/damus/Views/Muting/AddMuteItemView.swift
@@ -0,0 +1,114 @@
+//
+// 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?
+
+ @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) {
+ Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.")
+ 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
+ new_text.removeFirst()
+ return .hashtag(Hashtag(hashtag: new_text), expiration_date)
+ } else {
+ return .word(new_text, expiration_date)
+ }
+ }()
+
+ if let mute_item {
+ // Actually update & relay the new mute list
+ guard
+ let full_keypair = state.keypair.to_full(),
+ let existing_mutelist = state.contacts.mutelist,
+ let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item)
+ else {
+ return
+ }
+
+ state.contacts.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)
+ }
+}
diff --git a/damus/Views/Muting/MuteDurationMenu.swift b/damus/Views/Muting/MuteDurationMenu.swift
new file mode 100644
index 00000000..11498264
--- /dev/null
+++ b/damus/Views/Muting/MuteDurationMenu.swift
@@ -0,0 +1,40 @@
+//
+// 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 {
+ Button {
+ action(nil)
+ } label: {
+ Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.")
+ }
+ ForEach(DamusDuration.allCases, id: \.self) { duration in
+ Button {
+ action(duration)
+ } label: {
+ Text("\(duration.title)")
+ }
+ }
+ } label: {
+ self.label()
+ }
+ }
+}
+
+#Preview {
+ MuteDurationMenu { _ in
+
+ } label: {
+ Text("Mute hashtag")
+ }
+}
diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift
index c914c150..37813383 100644
--- a/damus/Views/Muting/MutelistView.swift
+++ b/damus/Views/Muting/MutelistView.swift
@@ -9,55 +9,117 @@ import SwiftUI
struct MutelistView: View {
let damus_state: DamusState
- @State var users: [Pubkey]
-
- func RemoveAction(pubkey: Pubkey) -> some View {
+ @State var mutelist_items: Set<MuteItem> = Set<MuteItem>()
+ @State var show_add_muteitem: Bool = false
+
+ func RemoveAction(item: MuteItem) -> some View {
Button {
guard let mutelist = damus_state.contacts.mutelist,
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.postbox.send(new_ev)
- users = get_mutelist_users(new_ev)
+ mutelist_items = new_ev.mute_list ?? Set<MuteItem>()
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
}
.tint(.red)
}
-
+
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(mutelist_items.users, id: \.self) { pubkey in
+ 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))
+ }
+ }
+ }
+ Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) {
+ ForEach(mutelist_items.hashtags, id: \.hashtag) { hashtag in
+ 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]))))
+ }
}
- .onTapGesture {
- damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
+ }
+ Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) {
+ ForEach(mutelist_items.words, id: \.self) { word in
+ 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(mutelist_items.threads, id: \.self) { note_id in
+ 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)
+ mutelist_items = damus_state.contacts.mutelist?.mute_list ?? Set<MuteItem>()
+ }
+ .onReceive(handle_notify(.new_mutes)) { new_mutes in
+ mutelist_items = mutelist_items.union(new_mutes)
+ }
+ .onReceive(handle_notify(.new_unmutes)) { new_unmutes in
+ mutelist_items = mutelist_items.subtracting(new_unmutes)
+ }
+ .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, mutelist_items: Set([
+ .user(test_note.pubkey, nil),
+ .hashtag(Hashtag(hashtag: "test"), nil),
+ .word("test", nil),
+ .thread(
test_note.id, nil)
+ ]))
}
}
diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift
index 47dc0fe8..e8f3e2f9 100644
--- a/damus/Views/Profile/ProfileView.swift
+++ b/damus/Views/Profile/ProfileView.swift
@@ -179,7 +179,7 @@ struct ProfileView: View {
notify(.report(.user(profile.pubkey)))
}
- if damus_state.contacts.is_muted(profile.pubkey) {
+ if damus_state.contacts.is_muted(.user(profile.pubkey, nil)) {
Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) {
guard
let keypair = damus_state.keypair.to_full(),
@@ -188,7 +188,7 @@ struct ProfileView: View {
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
}
@@ -196,8 +196,11 @@ struct ProfileView: View {
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)
}
}
}
diff --git a/damus/Views/Reposts/RepostedEvent.swift b/damus/Views/Reposts/RepostedEvent.swift
index 207ee295..6dc4e874 100644
--- a/damus/Views/Reposts/RepostedEvent.swift
+++ b/damus/Views/Reposts/RepostedEvent.swift
@@ -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
)
}) {
diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift
index 00ae46ea..91a15e2d 100644
--- a/damus/Views/SearchHomeView.swift
+++ b/damus/Views/SearchHomeView.swift
@@ -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.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil
+ if event_muted {
return false
}
diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift
index eb55cd6a..3d5fddf5 100644
--- a/damus/Views/SearchView.swift
+++ b/damus/Views/SearchView.swift
@@ -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,9 +42,70 @@ 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.contacts.mutelist,
+ let mutelist = remove_from_mutelist(keypair: full_keypair, prev: existing_mutelist, to_remove: .hashtag(Hashtag(hashtag: hashtag), nil))
+ else {
+ return
+ }
+
+ appstate.contacts.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.contacts.mutelist?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil))
+ }
}
}
+ func mute_hashtag(hashtag_string: String, expiration_time: Date?) {
+ guard
+ let full_keypair = appstate.keypair.to_full(),
+ let existing_mutelist = appstate.contacts.mutelist,
+ let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time))
+ else {
+ return
+ }
+
+ appstate.contacts.set_mutelist(mutelist)
+ appstate.postbox.send(mutelist)
+ }
+
var described_search: DescribedSearch {
return describe_search(search.search)
}
diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift
index ca5968f4..84abdc92 100644
--- a/damus/Views/SideMenuView.swift
+++ b/damus/Views/SideMenuView.swift
@@ -66,7 +66,7 @@ struct SideMenuView: View {
}
}
- NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
+ NavigationLink(value: Route.MuteList(mutelist_items: damus_state.contacts.mutelist?.mute_list ?? Set<MuteItem>())) {
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
}
diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift
index 7ef8a315..677107a9 100644
--- a/damus/Views/ThreadView.swift
+++ b/damus/Views/ThreadView.swift
@@ -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)
)
}
diff --git a/damusTests/ListTests.swift b/damusTests/ListTests.swift
index 4c9ae0c4..d457bffe 100644
--- a/damusTests/ListTests.swift
+++ b/damusTests/ListTests.swift
@@ -23,15 +23,13 @@ final class ListTests: XCTestCase {
let pubkey = test_keypair_full.pubkey
let to_mute = test_pubkey
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
- let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))!
XCTAssertEqual(mutelist.pubkey, pubkey)
XCTAssertEqual(mutelist.content, "")
- XCTAssertEqual(mutelist.tags.count, 2)
- XCTAssertEqual(mutelist.tags[0][0].string(), "d")
- XCTAssertEqual(mutelist.tags[0][1].string(), "mute")
- XCTAssertEqual(mutelist.tags[1][0].string(), "p")
- XCTAssertEqual(mutelist.tags[1][1].string(), to_mute.hex())
+ XCTAssertEqual(mutelist.tags.count, 1)
+ XCTAssertEqual(mutelist.tags[0][0].string(), "p")
+ XCTAssertEqual(mutelist.tags[0][1].string(), to_mute.hex())
}
func testCreateAndRemoveMuteList() throws {
@@ -39,14 +37,12 @@ final class ListTests: XCTestCase {
let pubkey = test_keypair_full.pubkey
let to_mute = test_pubkey
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
- let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
- let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(to_mute))!
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))!
+ let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(to_mute, nil))!
XCTAssertEqual(new.pubkey, pubkey)
XCTAssertEqual(new.content, "")
- XCTAssertEqual(new.tags.count, 1)
- XCTAssertEqual(new.tags[0][0].string(), "d")
- XCTAssertEqual(new.tags[0][1].string(), "mute")
+ XCTAssertEqual(new.tags.count, 0)
}
func testAddToExistingMutelist() throws {
@@ -55,17 +51,25 @@ final class ListTests: XCTestCase {
let to_mute = test_pubkey
let to_mute_2 = test_pubkey_2
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
- let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))!
- let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .pubkey(to_mute_2))!
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))!
+ let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .user(to_mute_2, nil))!
XCTAssertEqual(new.pubkey, pubkey)
XCTAssertEqual(new.content, "")
- XCTAssertEqual(new.tags.count, 3)
- XCTAssertEqual(new.tags[0][0].string(), "d")
- XCTAssertEqual(new.tags[0][1].string(), "mute")
+ XCTAssertEqual(new.tags.count, 2)
+ XCTAssertEqual(new.tags[0][0].string(), "p")
XCTAssertEqual(new.tags[1][0].string(), "p")
- XCTAssertEqual(new.tags[1][1].string(), to_mute.hex())
- XCTAssertEqual(new.tags[2][0].string(), "p")
- XCTAssertEqual(new.tags[2][1].string(), to_mute_2.hex())
+ // This test failed once out of like 10 tries, due to the tags being in the incorrect order. So I decided to put the elements in an array and sort it. That way if the mutelist tags aren't in the expected order it won't fail the test.
+ XCTAssertEqual([new.tags[0][1].string(), new.tags[1][1].string()].sorted(), [to_mute.hex(), to_mute_2.hex()].sorted())
+ }
+
+ func testAddToExistingMutelistShouldNotOverrideContent() throws {
+ let privkey = test_keypair_full.privkey
+ let pubkey = test_keypair_full.pubkey
+ let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
+ let mutelist = NostrEvent(content: "random", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: [])
+ let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .user(test_pubkey, nil))!
+
+ XCTAssertEqual(new.content, "random")
}
}
diff --git a/damusTests/LongPostTests.swift b/damusTests/LongPostTests.swift
index f4888c9f..70637607 100644
--- a/damusTests/LongPostTests.swift
+++ b/damusTests/LongPostTests.swift
@@ -34,7 +34,7 @@ final class LongPostTests: XCTestCase {
XCTAssertEqual(subid, "subid")
XCTAssertTrue(ev.should_show_event)
XCTAssertTrue(!ev.too_big)
- XCTAssertTrue(should_show_event(keypair: test_keypair, hellthreads: test_damus_state.muted_threads, contacts: contacts, ev: ev))
+ XCTAssertTrue(should_show_event(contacts: contacts, ev: ev))
XCTAssertTrue(validate_event(ev: ev) == .ok )
}
diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift
index ebdd587e..037c68db 100644
--- a/damusTests/Mocking/MockDamusState.swift
+++ b/damusTests/Mocking/MockDamusState.swift
@@ -44,7 +44,6 @@ func generate_test_damus_state(
postbox: .init(pool: pool),
bootstrap_relays: .init(),
replies: .init(our_pubkey: our_pubkey),
- muted_threads: .init(keypair: test_keypair),
wallet: .init(settings: settings),
nav: .init(),
music: .init(onChange: {_ in }),
diff --git a/damusTests/Models/MutedItemTests.swift b/damusTests/Models/MutedItemTests.swift
new file mode 100644
index 00000000..252d8b0c
--- /dev/null
+++ b/damusTests/Models/MutedItemTests.swift
@@ -0,0 +1,58 @@
+//
+// MutedItemTests.swift
+// damusTests
+//
+// Created by Charlie Fish on 1/14/24.
+//
+
+import XCTest
+@testable import damus
+
+class MutedItemTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ // MARK: - `is_expired`
+ func test_hashtag_is_expired() throws {
+ XCTAssertTrue(MuteItem.hashtag(Hashtag(hashtag: "test"), Date(timeIntervalSince1970: 0)).is_expired())
+ XCTAssertTrue(MuteItem.hashtag(Hashtag(hashtag: "test"), .distantPast).is_expired())
+ XCTAssertFalse(MuteItem.hashtag(Hashtag(hashtag: "test"), .distantFuture).is_expired())
+ }
+ func test_user_is_expired() throws {
+ XCTAssertTrue(MuteItem.user(test_pubkey, Date(timeIntervalSince1970: 0)).is_expired())
+ XCTAssertTrue(MuteItem.user(test_pubkey, .distantPast).is_expired())
+ XCTAssertFalse(MuteItem.user(test_pubkey, .distantFuture).is_expired())
+ }
+ func test_word_is_expired() throws {
+ XCTAssertTrue(MuteItem.word("test", Date(timeIntervalSince1970: 0)).is_expired())
+ XCTAssertTrue(MuteItem.word("test", .distantPast).is_expired())
+ XCTAssertFalse(MuteItem.word("test", .distantFuture).is_expired())
+ }
+ func test_thread_is_expired() throws {
+ XCTAssertTrue(MuteItem.thread(
test_note.id, Date(timeIntervalSince1970: 0)).is_expired())
+ XCTAssertTrue(MuteItem.thread(
test_note.id, .distantPast).is_expired())
+ XCTAssertFalse(MuteItem.thread(
test_note.id, .distantFuture).is_expired())
+ }
+
+
+ // MARK: - `tag`
+ func test_hashtag_tag() throws {
+ XCTAssertEqual(MuteItem.hashtag(Hashtag(hashtag: "test"), nil).tag, ["t", "test"])
+ XCTAssertEqual(MuteItem.hashtag(Hashtag(hashtag: "test"), Date(timeIntervalSince1970: 1704067200)).tag, ["t", "test", "1704067200"])
+ }
+ func test_user_tag() throws {
+ XCTAssertEqual(MuteItem.user(test_pubkey, Date(timeIntervalSince1970: 1704067200)).tag, ["p", test_pubkey.hex(), "1704067200"])
+ }
+ func test_word_tag() throws {
+ XCTAssertEqual(MuteItem.word("test", Date(timeIntervalSince1970: 1704067200)).tag, ["word", "test", "1704067200"])
+ }
+ func test_thread_tag() throws {
+ XCTAssertEqual(MuteItem.thread(
test_note.id, Date(timeIntervalSince1970: 1704067200)).tag, ["e", test_note.id.hex(), "1704067200"])
+ }
+}
--
2.39.3 (Apple Git-145)