mirror of
git://jb55.com/damus
synced 2024-09-30 00:40:45 +00:00
Merge improved mute functionality from Charlie
This merge adds a bunch of new features from charlie's work on the new mutelist changes: - Muted words - Mute performance optimizations - New mute list UI I needed to make a few changes to fix the tests in this merge. Otherwise it seems to work ok! Thank to Charlie for getting all of this working after many rounds of review! * branch `mute` of https://github.com/damus-io/damus: mute: fix bug with duplicate Indefinite items in MuteDurationMenu mute: fix mute hashtag from search view if no existing mutelist mute: integrate new MutelistManager mute: adding MutelistManager.swift mute: add maybe_get_content function to NdbNote mute: fix bug where mutes can't be added without existing mutelist mute: fix issue with not being able to change mute duration mute: don't mutate string when adding hashtag mute: implement fast MuteItem decoder tags: add u64 decoding function mute: migrating muted_threads to new mute list mute: adding ability to mute hashtag from SearchView mute: updating UI to support new mute list mute: adding filtering support for MuteItem events mute: receiving New Mute List Type mute: migrate Lists.swift to use new MuteItem mute: add new UI views for new mute list mute: adding new structs/enums for new mute list Changelog-Added: Add ability to mute words, add new mutelist interface (Charlie)
This commit is contained in:
commit
55000e9d4d
@ -11,7 +11,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
|||||||
let ndb: Ndb
|
let ndb: Ndb
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
let contacts: Contacts
|
let contacts: Contacts
|
||||||
let muted_threads: MutedThreadsManager
|
let mutelist_manager: MutelistManager
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
let zaps: Zaps
|
let zaps: Zaps
|
||||||
@ -28,7 +28,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
|||||||
self.settings = UserSettingsStore()
|
self.settings = UserSettingsStore()
|
||||||
|
|
||||||
self.contacts = Contacts(our_pubkey: keypair.pubkey)
|
self.contacts = Contacts(our_pubkey: keypair.pubkey)
|
||||||
self.muted_threads = MutedThreadsManager(keypair: keypair)
|
self.mutelist_manager = MutelistManager()
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.profiles = Profiles(ndb: ndb)
|
self.profiles = Profiles(ndb: ndb)
|
||||||
self.zaps = Zaps(our_pubkey: keypair.pubkey)
|
self.zaps = Zaps(our_pubkey: keypair.pubkey)
|
||||||
|
@ -422,10 +422,20 @@
|
|||||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||||
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
|
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
|
||||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
|
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
|
||||||
|
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */; };
|
||||||
|
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */; };
|
||||||
|
B533694E2B66D791008A805E /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
|
||||||
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
|
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
|
||||||
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
|
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
|
||||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
|
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
|
||||||
|
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
|
||||||
|
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; };
|
||||||
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
|
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
|
||||||
|
BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; };
|
||||||
|
BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.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 */; };
|
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||||
@ -514,7 +524,6 @@
|
|||||||
D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||||
D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
|
D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
|
||||||
D7CB5D422B116F8900AD4105 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.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 */; };
|
D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D442B116FE800AD4105 /* Contacts+.swift */; };
|
||||||
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; };
|
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; };
|
||||||
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||||
@ -1330,10 +1339,18 @@
|
|||||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||||
|
B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddMuteItemView.swift; sourceTree = "<group>"; };
|
||||||
|
B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuteDurationMenu.swift; sourceTree = "<group>"; };
|
||||||
|
B533694D2B66D791008A805E /* MutelistManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistManager.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||||
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
|
||||||
|
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||||
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
|
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||||
|
BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
|
||||||
|
BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
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>"; };
|
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||||
@ -1618,6 +1635,8 @@
|
|||||||
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */,
|
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */,
|
||||||
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */,
|
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */,
|
||||||
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
|
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
|
||||||
|
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
|
||||||
|
B533694D2B66D791008A805E /* MutelistManager.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2323,6 +2342,7 @@
|
|||||||
4CC14FED2A73FCBB007AEB17 /* Ids */,
|
4CC14FED2A73FCBB007AEB17 /* Ids */,
|
||||||
7527271D2A93FF0100214108 /* Block.swift */,
|
7527271D2A93FF0100214108 /* Block.swift */,
|
||||||
D798D21D2B0858BB00234419 /* MigratedTypes.swift */,
|
D798D21D2B0858BB00234419 /* MigratedTypes.swift */,
|
||||||
|
B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */,
|
||||||
);
|
);
|
||||||
path = Types;
|
path = Types;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2579,6 +2599,8 @@
|
|||||||
4CF0ABDF2981A83000D66079 /* Muting */ = {
|
4CF0ABDF2981A83000D66079 /* Muting */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */,
|
||||||
|
B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */,
|
||||||
4CF0ABE02981A83900D66079 /* MutelistView.swift */,
|
4CF0ABE02981A83900D66079 /* MutelistView.swift */,
|
||||||
);
|
);
|
||||||
path = Muting;
|
path = Muting;
|
||||||
@ -2736,6 +2758,7 @@
|
|||||||
children = (
|
children = (
|
||||||
F944F56D29EA9CCC0067B3BF /* DamusParseContentTests.swift */,
|
F944F56D29EA9CCC0067B3BF /* DamusParseContentTests.swift */,
|
||||||
75AD872A2AA23A460085EF2C /* Block+Tests.swift */,
|
75AD872A2AA23A460085EF2C /* Block+Tests.swift */,
|
||||||
|
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -3000,6 +3023,7 @@
|
|||||||
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */,
|
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */,
|
||||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
|
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
|
||||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
|
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
|
||||||
|
B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */,
|
||||||
4C32B9522A9AD44700DC3548 /* Message.swift in Sources */,
|
4C32B9522A9AD44700DC3548 /* Message.swift in Sources */,
|
||||||
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
|
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
|
||||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
||||||
@ -3080,6 +3104,7 @@
|
|||||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
|
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
|
||||||
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
|
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
|
||||||
|
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */,
|
||||||
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */,
|
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */,
|
||||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
|
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
|
||||||
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
|
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
|
||||||
@ -3110,6 +3135,7 @@
|
|||||||
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
|
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
|
||||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||||
BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */,
|
BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */,
|
||||||
|
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */,
|
||||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||||
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
|
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
|
||||||
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
||||||
@ -3323,6 +3349,7 @@
|
|||||||
D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */,
|
D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */,
|
||||||
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
|
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
|
||||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
||||||
|
B533694E2B66D791008A805E /* MutelistManager.swift in Sources */,
|
||||||
4C32B9532A9AD44700DC3548 /* Verifier.swift in Sources */,
|
4C32B9532A9AD44700DC3548 /* Verifier.swift in Sources */,
|
||||||
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
|
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
|
||||||
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */,
|
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */,
|
||||||
@ -3349,6 +3376,7 @@
|
|||||||
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
|
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
|
||||||
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
|
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
|
||||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
|
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
|
||||||
|
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||||
@ -3454,6 +3482,7 @@
|
|||||||
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
|
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
|
||||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
||||||
|
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
|
||||||
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
||||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||||
@ -3579,7 +3608,6 @@
|
|||||||
D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */,
|
D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */,
|
||||||
D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */,
|
D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */,
|
||||||
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
|
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
|
||||||
D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */,
|
|
||||||
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
||||||
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
|
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
|
||||||
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
|
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
|
||||||
@ -3618,6 +3646,7 @@
|
|||||||
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */,
|
D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */,
|
||||||
D7CE1B412B0BE719002EDAD4 /* FlatBuffersUtils.swift in Sources */,
|
D7CE1B412B0BE719002EDAD4 /* FlatBuffersUtils.swift in Sources */,
|
||||||
D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */,
|
D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */,
|
||||||
|
B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */,
|
||||||
D798D2262B085C4200234419 /* Bech32.swift in Sources */,
|
D798D2262B085C4200234419 /* Bech32.swift in Sources */,
|
||||||
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */,
|
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */,
|
||||||
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */,
|
D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */,
|
||||||
@ -3625,6 +3654,7 @@
|
|||||||
D7CE1B1E2B0BE190002EDAD4 /* midl.c in Sources */,
|
D7CE1B1E2B0BE190002EDAD4 /* midl.c in Sources */,
|
||||||
D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */,
|
D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */,
|
||||||
D7CE1B2D2B0BE250002EDAD4 /* take.c in Sources */,
|
D7CE1B2D2B0BE250002EDAD4 /* take.c in Sources */,
|
||||||
|
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,7 @@ struct ContentView: View {
|
|||||||
@State var active_sheet: Sheets? = nil
|
@State var active_sheet: Sheets? = nil
|
||||||
@State var damus_state: DamusState!
|
@State var damus_state: DamusState!
|
||||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
@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 confirm_mute: Bool = false
|
||||||
@State var hide_bar: Bool = false
|
@State var hide_bar: Bool = false
|
||||||
@State var user_muted_confirm: Bool = false
|
@State var user_muted_confirm: Bool = false
|
||||||
@ -384,8 +384,8 @@ struct ContentView: View {
|
|||||||
.onReceive(handle_notify(.report)) { target in
|
.onReceive(handle_notify(.report)) { target in
|
||||||
self.active_sheet = .report(target)
|
self.active_sheet = .report(target)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.mute)) { pubkey in
|
.onReceive(handle_notify(.mute)) { mute_item in
|
||||||
self.muting = pubkey
|
self.muting = mute_item
|
||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||||
@ -563,7 +563,7 @@ struct ContentView: View {
|
|||||||
user_muted_confirm = false
|
user_muted_confirm = false
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if let pubkey = self.muting {
|
if case let .user(pubkey, _) = self.muting {
|
||||||
let profile_txn = damus_state!.profiles.lookup(id: pubkey)
|
let profile_txn = damus_state!.profiles.lookup(id: pubkey)
|
||||||
let profile = profile_txn?.unsafeUnownedValue
|
let profile = profile_txn?.unsafeUnownedValue
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
@ -581,13 +581,13 @@ struct ContentView: View {
|
|||||||
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||||
guard let ds = damus_state,
|
guard let ds = damus_state,
|
||||||
let keypair = ds.keypair.to_full(),
|
let keypair = ds.keypair.to_full(),
|
||||||
let pubkey = muting,
|
let muting,
|
||||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
|
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting)
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state?.contacts.set_mutelist(mutelist)
|
ds.mutelist_manager.set_mutelist(mutelist)
|
||||||
ds.postbox.send(mutelist)
|
ds.postbox.send(mutelist)
|
||||||
|
|
||||||
confirm_overwrite_mutelist = false
|
confirm_overwrite_mutelist = false
|
||||||
@ -606,25 +606,25 @@ struct ContentView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds.contacts.mutelist == nil {
|
if ds.mutelist_manager.event == nil {
|
||||||
confirm_overwrite_mutelist = true
|
confirm_overwrite_mutelist = true
|
||||||
} else {
|
} else {
|
||||||
guard let keypair = ds.keypair.to_full(),
|
guard let keypair = ds.keypair.to_full(),
|
||||||
let pubkey = muting
|
let muting
|
||||||
else {
|
else {
|
||||||
return
|
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.mutelist_manager.event, to_add: muting) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state?.contacts.set_mutelist(ev)
|
ds.mutelist_manager.set_mutelist(ev)
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if let pubkey = muting {
|
if case let .user(pubkey, _) = muting {
|
||||||
let profile_txn = damus_state?.profiles.lookup(id: pubkey)
|
let profile_txn = damus_state?.profiles.lookup(id: pubkey)
|
||||||
let profile = profile_txn?.unsafeUnownedValue
|
let profile = profile_txn?.unsafeUnownedValue
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
@ -697,6 +697,7 @@ struct ContentView: View {
|
|||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
contacts: Contacts(our_pubkey: pubkey),
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
|
mutelist_manager: MutelistManager(),
|
||||||
profiles: Profiles(ndb: ndb),
|
profiles: Profiles(ndb: ndb),
|
||||||
dms: home.dms,
|
dms: home.dms,
|
||||||
previews: PreviewCache(),
|
previews: PreviewCache(),
|
||||||
@ -711,7 +712,6 @@ struct ContentView: View {
|
|||||||
postbox: PostBox(pool: pool),
|
postbox: PostBox(pool: pool),
|
||||||
bootstrap_relays: bootstrap_relays,
|
bootstrap_relays: bootstrap_relays,
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings),
|
||||||
nav: self.navigationCoordinator,
|
nav: self.navigationCoordinator,
|
||||||
music: MusicController(onChange: music_changed),
|
music: MusicController(onChange: music_changed),
|
||||||
@ -1153,7 +1153,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
result(.event(ev))
|
result(.event(ev))
|
||||||
}
|
}
|
||||||
case .hashtag(let ht):
|
case .hashtag(let ht):
|
||||||
result(.filter(.filter_hashtag([ht.string()])))
|
result(.filter(.filter_hashtag([ht.hashtag])))
|
||||||
case .param, .quote:
|
case .param, .quote:
|
||||||
// doesn't really make sense here
|
// doesn't really make sense here
|
||||||
break
|
break
|
||||||
|
@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
|||||||
return contacts.references.contains { ref in
|
return contacts.references.contains { ref in
|
||||||
switch (ref, follow) {
|
switch (ref, follow) {
|
||||||
case let (.hashtag(ht), .hashtag(follow_ht)):
|
case let (.hashtag(ht), .hashtag(follow_ht)):
|
||||||
return ht.string() == follow_ht
|
return ht.hashtag == follow_ht
|
||||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||||
return pk == follow_pk
|
return pk == follow_pk
|
||||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||||
|
@ -13,51 +13,14 @@ class Contacts {
|
|||||||
private var friend_of_friends: Set<Pubkey> = Set()
|
private var friend_of_friends: Set<Pubkey> = Set()
|
||||||
/// Tracks which friends are friends of a given pubkey.
|
/// Tracks which friends are friends of a given pubkey.
|
||||||
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||||
private var muted: Set<Pubkey> = Set()
|
|
||||||
|
|
||||||
let our_pubkey: Pubkey
|
let our_pubkey: Pubkey
|
||||||
var event: NostrEvent?
|
var event: NostrEvent?
|
||||||
var mutelist: NostrEvent?
|
|
||||||
|
|
||||||
init(our_pubkey: Pubkey) {
|
init(our_pubkey: Pubkey) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_muted(_ pk: Pubkey) -> Bool {
|
|
||||||
return muted.contains(pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 diff = old.symmetricDifference(new)
|
|
||||||
|
|
||||||
var new_mutes = Set<Pubkey>()
|
|
||||||
var new_unmutes = Set<Pubkey>()
|
|
||||||
|
|
||||||
for d in diff {
|
|
||||||
if new.contains(d) {
|
|
||||||
new_mutes.insert(d)
|
|
||||||
} else {
|
|
||||||
new_unmutes.insert(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: set local mutelist here
|
|
||||||
self.muted = Set(ev.referenced_pubkeys)
|
|
||||||
|
|
||||||
if new_mutes.count > 0 {
|
|
||||||
notify(.new_mutes(new_mutes))
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_unmutes.count > 0 {
|
|
||||||
notify(.new_unmutes(new_unmutes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func remove_friend(_ pubkey: Pubkey) {
|
func remove_friend(_ pubkey: Pubkey) {
|
||||||
friends.remove(pubkey)
|
friends.remove(pubkey)
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
|
|||||||
guard ev.known_kind == .boost else { return true }
|
guard ev.known_kind == .boost else { return true }
|
||||||
// This needs to use cached because it can be way too slow otherwise
|
// 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 }
|
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(state: damus_state, ev: inner_ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ class DamusState: HeadlessDamusState {
|
|||||||
let likes: EventCounter
|
let likes: EventCounter
|
||||||
let boosts: EventCounter
|
let boosts: EventCounter
|
||||||
let contacts: Contacts
|
let contacts: Contacts
|
||||||
|
let mutelist_manager: MutelistManager
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
let dms: DirectMessagesModel
|
let dms: DirectMessagesModel
|
||||||
let previews: PreviewCache
|
let previews: PreviewCache
|
||||||
@ -28,7 +29,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
let postbox: PostBox
|
let postbox: PostBox
|
||||||
let bootstrap_relays: [String]
|
let bootstrap_relays: [String]
|
||||||
let replies: ReplyCounter
|
let replies: ReplyCounter
|
||||||
let muted_threads: MutedThreadsManager
|
|
||||||
let wallet: WalletModel
|
let wallet: WalletModel
|
||||||
let nav: NavigationCoordinator
|
let nav: NavigationCoordinator
|
||||||
let music: MusicController?
|
let music: MusicController?
|
||||||
@ -36,12 +36,13 @@ class DamusState: HeadlessDamusState {
|
|||||||
let ndb: Ndb
|
let ndb: Ndb
|
||||||
var purple: DamusPurple
|
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, mutelist_manager: MutelistManager, 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.pool = pool
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.contacts = contacts
|
self.contacts = contacts
|
||||||
|
self.mutelist_manager = mutelist_manager
|
||||||
self.profiles = profiles
|
self.profiles = profiles
|
||||||
self.dms = dms
|
self.dms = dms
|
||||||
self.previews = previews
|
self.previews = previews
|
||||||
@ -56,7 +57,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.postbox = postbox
|
self.postbox = postbox
|
||||||
self.bootstrap_relays = bootstrap_relays
|
self.bootstrap_relays = bootstrap_relays
|
||||||
self.replies = replies
|
self.replies = replies
|
||||||
self.muted_threads = muted_threads
|
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.nav = nav
|
self.nav = nav
|
||||||
self.music = music
|
self.music = music
|
||||||
@ -110,6 +110,7 @@ class DamusState: HeadlessDamusState {
|
|||||||
likes: EventCounter(our_pubkey: empty_pub),
|
likes: EventCounter(our_pubkey: empty_pub),
|
||||||
boosts: EventCounter(our_pubkey: empty_pub),
|
boosts: EventCounter(our_pubkey: empty_pub),
|
||||||
contacts: Contacts(our_pubkey: empty_pub),
|
contacts: Contacts(our_pubkey: empty_pub),
|
||||||
|
mutelist_manager: MutelistManager(),
|
||||||
profiles: Profiles(ndb: .empty),
|
profiles: Profiles(ndb: .empty),
|
||||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||||
previews: PreviewCache(),
|
previews: PreviewCache(),
|
||||||
@ -124,7 +125,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||||
bootstrap_relays: [],
|
bootstrap_relays: [],
|
||||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||||
muted_threads: MutedThreadsManager(keypair: kp),
|
|
||||||
wallet: WalletModel(settings: UserSettingsStore()),
|
wallet: WalletModel(settings: UserSettingsStore()),
|
||||||
nav: NavigationCoordinator(),
|
nav: NavigationCoordinator(),
|
||||||
music: nil,
|
music: nil,
|
||||||
|
@ -15,7 +15,7 @@ protocol HeadlessDamusState {
|
|||||||
var ndb: Ndb { get }
|
var ndb: Ndb { get }
|
||||||
var settings: UserSettingsStore { get }
|
var settings: UserSettingsStore { get }
|
||||||
var contacts: Contacts { get }
|
var contacts: Contacts { get }
|
||||||
var muted_threads: MutedThreadsManager { get }
|
var mutelist_manager: MutelistManager { get }
|
||||||
var keypair: Keypair { get }
|
var keypair: Keypair { get }
|
||||||
var profiles: Profiles { get }
|
var profiles: Profiles { get }
|
||||||
var zaps: Zaps { get }
|
var zaps: Zaps { get }
|
||||||
|
@ -157,8 +157,10 @@ class HomeModel {
|
|||||||
case .metadata:
|
case .metadata:
|
||||||
// profile metadata processing is handled by nostrdb
|
// profile metadata processing is handled by nostrdb
|
||||||
break
|
break
|
||||||
case .list:
|
case .list_deprecated:
|
||||||
handle_list_event(ev)
|
handle_old_list_event(ev)
|
||||||
|
case .mute_list:
|
||||||
|
handle_mute_list_event(ev)
|
||||||
case .boost:
|
case .boost:
|
||||||
handle_boost_event(sub_id: sub_id, ev)
|
handle_boost_event(sub_id: sub_id, ev)
|
||||||
case .like:
|
case .like:
|
||||||
@ -242,7 +244,7 @@ class HomeModel {
|
|||||||
process_zap_event(state: damus_state, ev: ev) { zapres in
|
process_zap_event(state: damus_state, ev: ev) { zapres in
|
||||||
guard case .done(let zap) = zapres,
|
guard case .done(let zap) = zapres,
|
||||||
zap.target.pubkey == self.damus_state.keypair.pubkey,
|
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(state: self.damus_state, ev: zap.request.ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,11 +278,11 @@ class HomeModel {
|
|||||||
|
|
||||||
func filter_events() {
|
func filter_events() {
|
||||||
events.filter { ev in
|
events.filter { ev in
|
||||||
!damus_state.contacts.is_muted(ev.pubkey)
|
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dms.dms = dms.dms.filter { ev in
|
self.dms.dms = dms.dms.filter { ev in
|
||||||
!damus_state.contacts.is_muted(ev.pubkey)
|
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.filter { ev in
|
notifications.filter { ev in
|
||||||
@ -288,7 +290,8 @@ class HomeModel {
|
|||||||
return false
|
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.mutelist_manager.is_event_muted(ev)
|
||||||
|
return !event_muted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,8 +464,11 @@ class HomeModel {
|
|||||||
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
||||||
our_contacts_filter.authors = [damus_state.pubkey]
|
our_contacts_filter.authors = [damus_state.pubkey]
|
||||||
|
|
||||||
var our_blocklist_filter = NostrFilter(kinds: [.list])
|
var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated])
|
||||||
our_blocklist_filter.parameter = ["mute"]
|
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]
|
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||||
|
|
||||||
var dms_filter = NostrFilter(kinds: [.dm])
|
var dms_filter = NostrFilter(kinds: [.dm])
|
||||||
@ -488,7 +494,7 @@ class HomeModel {
|
|||||||
notifications_filter.limit = 500
|
notifications_filter.limit = 500
|
||||||
|
|
||||||
var notifications_filters = [notifications_filter]
|
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]
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||||
|
|
||||||
@ -557,13 +563,32 @@ class HomeModel {
|
|||||||
pool.send(.subscribe(sub), to: relay_ids)
|
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.mutelist_manager.event {
|
||||||
|
if ev.created_at <= mutelist.created_at {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state.mutelist_manager.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
|
// we only care about our lists
|
||||||
guard ev.pubkey == damus_state.pubkey else {
|
guard ev.pubkey == damus_state.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let mutelist = damus_state.contacts.mutelist {
|
// we only care about the most recent mutelist
|
||||||
|
if let mutelist = damus_state.mutelist_manager.event {
|
||||||
if ev.created_at <= mutelist.created_at {
|
if ev.created_at <= mutelist.created_at {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -573,7 +598,9 @@ class HomeModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.contacts.set_mutelist(ev)
|
damus_state.mutelist_manager.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? {
|
func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? {
|
||||||
@ -589,7 +616,7 @@ class HomeModel {
|
|||||||
// don't show notifications from ourselves
|
// don't show notifications from ourselves
|
||||||
guard ev.pubkey != damus_state.pubkey,
|
guard ev.pubkey != damus_state.pubkey,
|
||||||
event_has_our_pubkey(ev, our_pubkey: self.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(state: damus_state, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -627,7 +654,7 @@ class HomeModel {
|
|||||||
|
|
||||||
|
|
||||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
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(state: damus_state, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,7 +683,7 @@ class HomeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_dm(_ ev: NostrEvent) {
|
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(state: damus_state, ev: ev) else {
|
||||||
return
|
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 {
|
func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
||||||
return should_show_event(
|
return should_show_event(
|
||||||
keypair: damus_state.keypair,
|
state: damus_state,
|
||||||
hellthreads: damus_state.muted_threads,
|
|
||||||
contacts: damus_state.contacts,
|
|
||||||
ev: event
|
ev: event
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool {
|
func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||||
if contacts.is_muted(ev.pubkey) {
|
let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair)
|
||||||
return false
|
if event_muted {
|
||||||
}
|
|
||||||
|
|
||||||
if hellthreads.isMutedThread(ev, keypair: keypair) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
202
damus/Models/MuteItem.swift
Normal file
202
damus/Models/MuteItem.swift
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
//
|
||||||
|
// MuteItem.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents an item that is muted.
|
||||||
|
enum MuteItem: Hashable, Equatable {
|
||||||
|
/// A user that is muted.
|
||||||
|
///
|
||||||
|
/// The associated type is the ``Pubkey`` that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||||
|
case user(Pubkey, Date?)
|
||||||
|
|
||||||
|
/// A hashtag that is muted.
|
||||||
|
///
|
||||||
|
/// The associated type is the hashtag string that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||||
|
case hashtag(Hashtag, Date?)
|
||||||
|
|
||||||
|
/// A word/phrase that is muted.
|
||||||
|
///
|
||||||
|
/// The associated type is the word/phrase that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||||
|
case word(String, Date?)
|
||||||
|
|
||||||
|
/// A thread that is muted.
|
||||||
|
///
|
||||||
|
/// The associated type is the `id` of the note that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||||
|
case thread(NoteId, Date?)
|
||||||
|
|
||||||
|
func is_expired() -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .user(_, let expiration_date):
|
||||||
|
return expiration_date ?? .distantFuture < Date()
|
||||||
|
case .hashtag(_, let expiration_date):
|
||||||
|
return expiration_date ?? .distantFuture < Date()
|
||||||
|
case .word(_, let expiration_date):
|
||||||
|
return expiration_date ?? .distantFuture < Date()
|
||||||
|
case .thread(_, let expiration_date):
|
||||||
|
return expiration_date ?? .distantFuture < Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: MuteItem, rhs: MuteItem) -> Bool {
|
||||||
|
// lhs is the item we want to check (ie. the item the user is attempting to display)
|
||||||
|
// rhs is the item we want to check against (ie. the item in the mute list)
|
||||||
|
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// - MARK: TagConvertible
|
||||||
|
extension MuteItem: TagConvertible {
|
||||||
|
enum MuteKeys: String {
|
||||||
|
case p, t, word, e
|
||||||
|
|
||||||
|
init?(tag: NdbTagElem) {
|
||||||
|
let len = tag.count
|
||||||
|
if len == 1 {
|
||||||
|
switch tag.single_char {
|
||||||
|
case "p": self = .p
|
||||||
|
case "t": self = .t
|
||||||
|
case "e": self = .e
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
} else if len == 4 && tag.matches_str("word", tag_len: 4) {
|
||||||
|
self = .word
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String { self.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from_tag(tag: TagSequence) -> MuteItem? {
|
||||||
|
guard tag.count >= 2 else { return nil }
|
||||||
|
|
||||||
|
var i = tag.makeIterator()
|
||||||
|
|
||||||
|
guard let t0 = i.next(),
|
||||||
|
let mkey = MuteKeys(tag: t0),
|
||||||
|
let t1 = i.next()
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiry: Date? = nil
|
||||||
|
if let expiry_str = i.next(), let ts = expiry_str.u64() {
|
||||||
|
expiry = Date(timeIntervalSince1970: Double(ts))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mkey {
|
||||||
|
case .p:
|
||||||
|
return t1.id().map({ .user(Pubkey($0), expiry) })
|
||||||
|
case .t:
|
||||||
|
return .hashtag(Hashtag(hashtag: t1.string()), expiry)
|
||||||
|
case .word:
|
||||||
|
return .word(t1.string(), expiry)
|
||||||
|
case .e:
|
||||||
|
guard let id = t1.id() else { return nil }
|
||||||
|
return .thread(NoteId(id), expiry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
|
|||||||
pk_setting_key(pubkey, key: "muted_threads")
|
pk_setting_key(pubkey, key: "muted_threads")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
func loadOldMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||||
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
|
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||||
return xs.reduce(into: [NoteId]()) { ids, k in
|
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 {
|
// We need to still use it since existing users might have their muted threads stored in UserDefaults
|
||||||
let uniqueMutedThreads = Array(Set(value))
|
// 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
|
||||||
if uniqueMutedThreads != currentValue {
|
func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) {
|
||||||
let ids = uniqueMutedThreads.map { note_id in return note_id.hex() }
|
// Ensure that keypair is fullkeypair
|
||||||
UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey))
|
guard let fullKeypair = keypair.to_full() else { return }
|
||||||
return true
|
// Load existing muted threads
|
||||||
}
|
let mutedThreads = loadOldMutedThreads(pubkey: fullKeypair.pubkey)
|
||||||
|
guard !mutedThreads.isEmpty else { return }
|
||||||
return false
|
// Set new muted system for those existing threads
|
||||||
}
|
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||||
|
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||||
class MutedThreadsManager: ObservableObject {
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||||
|
damus_state.postbox.send(new_mutelist_event)
|
||||||
private let keypair: Keypair
|
// Set existing muted threads to an empty array
|
||||||
|
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
162
damus/Models/MutelistManager.swift
Normal file
162
damus/Models/MutelistManager.swift
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
//
|
||||||
|
// MutelistManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/28/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class MutelistManager {
|
||||||
|
private(set) var event: NostrEvent? = nil
|
||||||
|
|
||||||
|
var users: Set<MuteItem> = []
|
||||||
|
var hashtags: Set<MuteItem> = []
|
||||||
|
var threads: Set<MuteItem> = []
|
||||||
|
var words: Set<MuteItem> = []
|
||||||
|
|
||||||
|
func refresh_sets() {
|
||||||
|
guard let referenced_mute_items = event?.referenced_mute_items else { return }
|
||||||
|
|
||||||
|
var new_users: Set<MuteItem> = []
|
||||||
|
var new_hashtags: Set<MuteItem> = []
|
||||||
|
var new_threads: Set<MuteItem> = []
|
||||||
|
var new_words: Set<MuteItem> = []
|
||||||
|
|
||||||
|
for mute_item in referenced_mute_items {
|
||||||
|
switch mute_item {
|
||||||
|
case .user:
|
||||||
|
new_users.insert(mute_item)
|
||||||
|
case .hashtag:
|
||||||
|
new_hashtags.insert(mute_item)
|
||||||
|
case .word:
|
||||||
|
new_words.insert(mute_item)
|
||||||
|
case .thread:
|
||||||
|
new_threads.insert(mute_item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users = new_users
|
||||||
|
hashtags = new_hashtags
|
||||||
|
threads = new_threads
|
||||||
|
words = new_words
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_muted(_ item: MuteItem) -> Bool {
|
||||||
|
switch item {
|
||||||
|
case .user(_, _):
|
||||||
|
return users.contains(item)
|
||||||
|
case .hashtag(_, _):
|
||||||
|
return hashtags.contains(item)
|
||||||
|
case .word(_, _):
|
||||||
|
return words.contains(item)
|
||||||
|
case .thread(_, _):
|
||||||
|
return threads.contains(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_event_muted(_ ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||||
|
return event_muted_reason(ev, keypair: keypair) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_mutelist(_ ev: NostrEvent) {
|
||||||
|
let oldlist = self.event
|
||||||
|
self.event = ev
|
||||||
|
|
||||||
|
let old: Set<MuteItem> = oldlist?.mute_list ?? Set<MuteItem>()
|
||||||
|
let new: Set<MuteItem> = ev.mute_list ?? Set<MuteItem>()
|
||||||
|
let diff = old.symmetricDifference(new)
|
||||||
|
|
||||||
|
var new_mutes = Set<MuteItem>()
|
||||||
|
var new_unmutes = Set<MuteItem>()
|
||||||
|
|
||||||
|
for d in diff {
|
||||||
|
if new.contains(d) {
|
||||||
|
add_mute_item(d)
|
||||||
|
new_mutes.insert(d)
|
||||||
|
} else {
|
||||||
|
remove_mute_item(d)
|
||||||
|
new_unmutes.insert(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_mutes.count > 0 {
|
||||||
|
notify(.new_mutes(new_mutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_unmutes.count > 0 {
|
||||||
|
notify(.new_unmutes(new_unmutes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func add_mute_item(_ item: MuteItem) {
|
||||||
|
switch item {
|
||||||
|
case .user(_, _):
|
||||||
|
users.insert(item)
|
||||||
|
case .hashtag(_, _):
|
||||||
|
hashtags.insert(item)
|
||||||
|
case .word(_, _):
|
||||||
|
words.insert(item)
|
||||||
|
case .thread(_, _):
|
||||||
|
threads.insert(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func remove_mute_item(_ item: MuteItem) {
|
||||||
|
switch item {
|
||||||
|
case .user(_, _):
|
||||||
|
users.remove(item)
|
||||||
|
case .hashtag(_, _):
|
||||||
|
hashtags.remove(item)
|
||||||
|
case .word(_, _):
|
||||||
|
words.remove(item)
|
||||||
|
case .thread(_, _):
|
||||||
|
threads.remove(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 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, keypair: Keypair? = nil) -> MuteItem? {
|
||||||
|
// Events from the current user should not be muted.
|
||||||
|
guard keypair?.pubkey != ev.pubkey else { return nil }
|
||||||
|
|
||||||
|
// Check if user is muted
|
||||||
|
let check_user_item = MuteItem.user(ev.pubkey, nil)
|
||||||
|
if users.contains(check_user_item) {
|
||||||
|
return check_user_item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if hashtag is muted
|
||||||
|
for hashtag in ev.referenced_hashtags {
|
||||||
|
let check_hashtag_item = MuteItem.hashtag(hashtag, nil)
|
||||||
|
if hashtags.contains(check_hashtag_item) {
|
||||||
|
return check_hashtag_item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if thread is muted
|
||||||
|
for thread_id in ev.referenced_ids {
|
||||||
|
let check_thread_item = MuteItem.thread(thread_id, nil)
|
||||||
|
if threads.contains(check_thread_item) {
|
||||||
|
return check_thread_item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if word is muted
|
||||||
|
if let keypair, let content: String = ev.maybe_get_content(keypair)?.lowercased() {
|
||||||
|
for word in words {
|
||||||
|
if case .word(let string, _) = word {
|
||||||
|
if content.contains(string.lowercased()) {
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -36,13 +36,8 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show notifications from muted threads.
|
// Don't show notifications that match mute list.
|
||||||
if state.muted_threads.isMutedThread(ev, keypair: state.keypair) {
|
if state.mutelist_manager.is_event_muted(ev, keypair: state.keypair) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show notifications from muted users
|
|
||||||
if state.contacts.is_muted(ev.pubkey) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class SearchHomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filter_muted() {
|
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(state: damus_state, ev: $0) }
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject {
|
|||||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||||
return
|
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(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair)
|
||||||
{
|
{
|
||||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||||
return
|
return
|
||||||
|
@ -28,7 +28,7 @@ class SearchModel: ObservableObject {
|
|||||||
|
|
||||||
func filter_muted() {
|
func filter_muted() {
|
||||||
self.events.filter {
|
self.events.filter {
|
||||||
should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: $0)
|
should_show_event(state: state, ev: $0)
|
||||||
}
|
}
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ class SearchModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: ev) else {
|
guard should_show_event(state: state, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -804,3 +804,15 @@ 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") })) || self.kind == NostrKind.mute_list.rawValue {
|
||||||
|
return Set(self.referenced_mute_items)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,7 +17,8 @@ enum NostrKind: UInt32, Codable {
|
|||||||
case boost = 6
|
case boost = 6
|
||||||
case like = 7
|
case like = 7
|
||||||
case chat = 42
|
case chat = 42
|
||||||
case list = 30000
|
case mute_list = 10000
|
||||||
|
case list_deprecated = 30000
|
||||||
case longform = 30023
|
case longform = 30023
|
||||||
case zap = 9735
|
case zap = 9735
|
||||||
case zap_request = 9734
|
case zap_request = 9734
|
||||||
|
@ -119,7 +119,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case event(NoteId)
|
case event(NoteId)
|
||||||
case pubkey(Pubkey)
|
case pubkey(Pubkey)
|
||||||
case quote(QuoteId)
|
case quote(QuoteId)
|
||||||
case hashtag(TagElem)
|
case hashtag(Hashtag)
|
||||||
case param(TagElem)
|
case param(TagElem)
|
||||||
case naddr(NAddr)
|
case naddr(NAddr)
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .event(let noteId): return noteId.hex()
|
case .event(let noteId): return noteId.hex()
|
||||||
case .pubkey(let pubkey): return pubkey.hex()
|
case .pubkey(let pubkey): return pubkey.hex()
|
||||||
case .quote(let quote): return quote.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()
|
case .param(let string): return string.string()
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
||||||
@ -176,7 +176,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .e: return t1.id().map({ .event(NoteId($0)) })
|
case .e: return t1.id().map({ .event(NoteId($0)) })
|
||||||
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
|
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
|
||||||
case .q: return t1.id().map({ .quote(QuoteId($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)
|
case .d: return .param(t1)
|
||||||
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct MuteNotify: Notify {
|
struct MuteNotify: Notify {
|
||||||
typealias Payload = Pubkey
|
typealias Payload = MuteItem
|
||||||
var payload: Payload
|
var payload: MuteItem
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotifyHandler {
|
extension NotifyHandler {
|
||||||
@ -19,7 +19,7 @@ extension NotifyHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Notifications {
|
extension Notifications {
|
||||||
static func mute(_ target: Pubkey) -> Notifications<MuteNotify> {
|
static func mute(_ target: MuteItem) -> Notifications<MuteNotify> {
|
||||||
.init(.init(payload: target))
|
.init(.init(payload: target))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct NewMutesNotify: Notify {
|
struct NewMutesNotify: Notify {
|
||||||
typealias Payload = Set<Pubkey>
|
typealias Payload = Set<MuteItem>
|
||||||
var payload: Payload
|
var payload: Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ extension NotifyHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Notifications {
|
extension Notifications {
|
||||||
static func new_mutes(_ pubkeys: Set<Pubkey>) -> Notifications<NewMutesNotify> {
|
static func new_mutes(_ pubkeys: Set<MuteItem>) -> Notifications<NewMutesNotify> {
|
||||||
.init(.init(payload: pubkeys))
|
.init(.init(payload: pubkeys))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct NewUnmutesNotify: Notify {
|
struct NewUnmutesNotify: Notify {
|
||||||
typealias Payload = Set<Pubkey>
|
typealias Payload = Set<MuteItem>
|
||||||
var payload: Payload
|
var payload: Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ extension NotifyHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Notifications {
|
extension Notifications {
|
||||||
static func new_unmutes(_ pubkeys: Set<Pubkey>) -> Notifications<NewUnmutesNotify> {
|
static func new_unmutes(_ pubkeys: Set<MuteItem>) -> Notifications<NewUnmutesNotify> {
|
||||||
.init(.init(payload: pubkeys))
|
.init(.init(payload: pubkeys))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ var test_damus_state: DamusState = ({
|
|||||||
likes: .init(our_pubkey: our_pubkey),
|
likes: .init(our_pubkey: our_pubkey),
|
||||||
boosts: .init(our_pubkey: our_pubkey),
|
boosts: .init(our_pubkey: our_pubkey),
|
||||||
contacts: .init(our_pubkey: our_pubkey),
|
contacts: .init(our_pubkey: our_pubkey),
|
||||||
|
mutelist_manager: MutelistManager(),
|
||||||
profiles: .init(ndb: ndb),
|
profiles: .init(ndb: ndb),
|
||||||
dms: .init(our_pubkey: our_pubkey),
|
dms: .init(our_pubkey: our_pubkey),
|
||||||
previews: .init(),
|
previews: .init(),
|
||||||
@ -87,7 +88,6 @@ var test_damus_state: DamusState = ({
|
|||||||
postbox: .init(pool: pool),
|
postbox: .init(pool: pool),
|
||||||
bootstrap_relays: .init(),
|
bootstrap_relays: .init(),
|
||||||
replies: .init(our_pubkey: our_pubkey),
|
replies: .init(our_pubkey: our_pubkey),
|
||||||
muted_threads: .init(keypair: test_keypair),
|
|
||||||
wallet: .init(settings: settings),
|
wallet: .init(settings: settings),
|
||||||
nav: .init(),
|
nav: .init(),
|
||||||
music: .init(onChange: {_ in }),
|
music: .init(onChange: {_ in }),
|
||||||
|
43
damus/Types/DamusDuration.swift
Normal file
43
damus/Types/DamusDuration.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// DamusDuration.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DamusDuration: CaseIterable {
|
||||||
|
case indefinite
|
||||||
|
case day
|
||||||
|
case week
|
||||||
|
case month
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .indefinite:
|
||||||
|
return NSLocalizedString("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.")
|
||||||
|
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 .indefinite:
|
||||||
|
return nil
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,64 +7,30 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId) -> NostrEvent? {
|
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: Set<MuteItem>) -> NostrEvent? {
|
||||||
return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p")
|
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? {
|
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: MuteItem) -> NostrEvent? {
|
||||||
return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove)
|
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? {
|
func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_remove: MuteItem) -> NostrEvent? {
|
||||||
if let prev = mprev,
|
let muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>()).subtracting([to_remove]).filter { !$0.is_expired() }
|
||||||
prev.pubkey == keypair.pubkey,
|
let tags: [[String]] = muted_items.map { $0.tag }
|
||||||
matches_list_name(tags: prev.tags, name: list_name)
|
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: tags)
|
||||||
{
|
}
|
||||||
return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add)
|
|
||||||
|
func toggle_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_toggle: MuteItem) -> NostrEvent? {
|
||||||
|
let existing_muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>())
|
||||||
|
|
||||||
|
if existing_muted_items.contains(to_toggle) {
|
||||||
|
// Already exists, remove
|
||||||
|
return remove_from_mutelist(keypair: keypair, prev: prev, to_remove: to_toggle)
|
||||||
|
} else {
|
||||||
|
// Doesn't exist, add
|
||||||
|
return create_or_update_mutelist(keypair: keypair, mprev: prev, to_add: to_toggle)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ enum Route: Hashable {
|
|||||||
case Relay(relay: String, showActionButtons: Binding<Bool>)
|
case Relay(relay: String, showActionButtons: Binding<Bool>)
|
||||||
case RelayDetail(relay: String, metadata: RelayMetadata?)
|
case RelayDetail(relay: String, metadata: RelayMetadata?)
|
||||||
case Following(following: FollowingModel)
|
case Following(following: FollowingModel)
|
||||||
case MuteList(users: [Pubkey])
|
case MuteList
|
||||||
case RelayConfig
|
case RelayConfig
|
||||||
case Script(script: ScriptModel)
|
case Script(script: ScriptModel)
|
||||||
case Bookmarks
|
case Bookmarks
|
||||||
@ -58,8 +58,8 @@ enum Route: Hashable {
|
|||||||
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
|
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
|
||||||
case .Following(let following):
|
case .Following(let following):
|
||||||
FollowingView(damus_state: damusState, following: following)
|
FollowingView(damus_state: damusState, following: following)
|
||||||
case .MuteList(let users):
|
case .MuteList:
|
||||||
MutelistView(damus_state: damusState, users: users)
|
MutelistView(damus_state: damusState)
|
||||||
case .RelayConfig:
|
case .RelayConfig:
|
||||||
RelayConfigView(state: damusState)
|
RelayConfigView(state: damusState)
|
||||||
case .Bookmarks:
|
case .Bookmarks:
|
||||||
@ -139,9 +139,8 @@ enum Route: Hashable {
|
|||||||
hasher.combine(relay)
|
hasher.combine(relay)
|
||||||
case .Following:
|
case .Following:
|
||||||
hasher.combine("following")
|
hasher.combine("following")
|
||||||
case .MuteList(let users):
|
case .MuteList:
|
||||||
hasher.combine("muteList")
|
hasher.combine("muteList")
|
||||||
hasher.combine(users)
|
|
||||||
case .RelayConfig:
|
case .RelayConfig:
|
||||||
hasher.combine("relayConfig")
|
hasher.combine("relayConfig")
|
||||||
case .Bookmarks:
|
case .Bookmarks:
|
||||||
|
@ -20,9 +20,9 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
ScrollViewReader { scroller in
|
ScrollViewReader { scroller in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading) {
|
LazyVStack(alignment: .leading) {
|
||||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0, keypair: damus_state.keypair)}, id: \.0.id) { (ev, ind) in
|
||||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
|
.contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
|
||||||
}
|
}
|
||||||
EndBlock(height: 1)
|
EndBlock(height: 1)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ struct DirectMessagesView: View {
|
|||||||
|
|
||||||
func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] {
|
func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] {
|
||||||
return dms.filter({ dm in
|
return dms.filter({ dm in
|
||||||
return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(dm.pubkey)
|
return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.mutelist_manager.is_muted(.user(dm.pubkey, nil))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ struct DirectMessagesView: View {
|
|||||||
|
|
||||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if let ev = model.events.last {
|
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0, keypair: damus_state.keypair) }) {
|
||||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
self.model.set_active_dm_model(model)
|
self.model.set_active_dm_model(model)
|
||||||
|
@ -8,21 +8,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EventMenuContext: View {
|
struct EventMenuContext: View {
|
||||||
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let keypair: Keypair
|
|
||||||
let target_pubkey: Pubkey
|
let target_pubkey: Pubkey
|
||||||
let bookmarks: BookmarksManager
|
|
||||||
let muted_threads: MutedThreadsManager
|
|
||||||
let profileModel : ProfileModel
|
let profileModel : ProfileModel
|
||||||
@ObservedObject var settings: UserSettingsStore
|
|
||||||
|
|
||||||
init(damus: DamusState, event: NostrEvent) {
|
init(damus: DamusState, event: NostrEvent) {
|
||||||
|
self.damus_state = damus
|
||||||
self.event = event
|
self.event = event
|
||||||
self.keypair = damus.keypair
|
|
||||||
self.target_pubkey = event.pubkey
|
self.target_pubkey = event.pubkey
|
||||||
self.bookmarks = damus.bookmarks
|
|
||||||
self.muted_threads = damus.muted_threads
|
|
||||||
self._settings = ObservedObject(wrappedValue: damus.settings)
|
|
||||||
self.profileModel = ProfileModel(pubkey: target_pubkey, damus: damus)
|
self.profileModel = ProfileModel(pubkey: target_pubkey, damus: damus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +28,7 @@ struct EventMenuContext: View {
|
|||||||
// Add our Menu button inside an overlay modifier to avoid affecting the rest of the layout around us.
|
// Add our Menu button inside an overlay modifier to avoid affecting the rest of the layout around us.
|
||||||
.overlay(
|
.overlay(
|
||||||
Menu {
|
Menu {
|
||||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings, profileModel: profileModel)
|
MenuItems(damus_state: damus_state, event: event, target_pubkey: target_pubkey, profileModel: profileModel)
|
||||||
} label: {
|
} label: {
|
||||||
Color.clear
|
Color.clear
|
||||||
}
|
}
|
||||||
@ -49,38 +43,31 @@ struct EventMenuContext: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct MenuItems: View {
|
struct MenuItems: View {
|
||||||
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let keypair: Keypair
|
|
||||||
let target_pubkey: Pubkey
|
let target_pubkey: Pubkey
|
||||||
let bookmarks: BookmarksManager
|
|
||||||
let muted_threads: MutedThreadsManager
|
|
||||||
let profileModel: ProfileModel
|
let profileModel: ProfileModel
|
||||||
|
|
||||||
@ObservedObject var settings: UserSettingsStore
|
|
||||||
|
|
||||||
@State private var isBookmarked: Bool = false
|
@State private var isBookmarked: Bool = false
|
||||||
@State private var isMutedThread: Bool = false
|
@State private var isMutedThread: Bool = false
|
||||||
|
|
||||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: Pubkey, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager, settings: UserSettingsStore, profileModel: ProfileModel) {
|
init(damus_state: DamusState, event: NostrEvent, target_pubkey: Pubkey, profileModel: ProfileModel) {
|
||||||
let bookmarked = bookmarks.isBookmarked(event)
|
let bookmarked = damus_state.bookmarks.isBookmarked(event)
|
||||||
self._isBookmarked = State(initialValue: bookmarked)
|
self._isBookmarked = State(initialValue: bookmarked)
|
||||||
|
|
||||||
let muted_thread = muted_threads.isMutedThread(event, keypair: keypair)
|
let muted_thread = damus_state.mutelist_manager.is_event_muted(event)
|
||||||
self._isMutedThread = State(initialValue: muted_thread)
|
self._isMutedThread = State(initialValue: muted_thread)
|
||||||
|
|
||||||
self.bookmarks = bookmarks
|
self.damus_state = damus_state
|
||||||
self.muted_threads = muted_threads
|
|
||||||
self.event = event
|
self.event = event
|
||||||
self.keypair = keypair
|
|
||||||
self.target_pubkey = target_pubkey
|
self.target_pubkey = target_pubkey
|
||||||
self.settings = settings
|
|
||||||
self.profileModel = profileModel
|
self.profileModel = profileModel
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = event.get_content(keypair)
|
UIPasteboard.general.string = event.get_content(damus_state.keypair)
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2")
|
Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2")
|
||||||
}
|
}
|
||||||
@ -97,7 +84,7 @@ struct MenuItems: View {
|
|||||||
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.developer_mode {
|
if damus_state.settings.developer_mode {
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = event_to_json(ev: event)
|
UIPasteboard.general.string = event_to_json(ev: event)
|
||||||
} label: {
|
} label: {
|
||||||
@ -106,8 +93,8 @@ struct MenuItems: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
self.bookmarks.updateBookmark(event)
|
self.damus_state.bookmarks.updateBookmark(event)
|
||||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
isBookmarked = self.damus_state.bookmarks.isBookmarked(event)
|
||||||
} label: {
|
} label: {
|
||||||
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
|
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||||
let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.")
|
let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.")
|
||||||
@ -122,9 +109,13 @@ struct MenuItems: View {
|
|||||||
}
|
}
|
||||||
// Mute thread - relocated to below Broadcast, as to move further away from Add Bookmark to prevent accidental muted threads
|
// Mute thread - relocated to below Broadcast, as to move further away from Add Bookmark to prevent accidental muted threads
|
||||||
if event.known_kind != .dm {
|
if event.known_kind != .dm {
|
||||||
Button {
|
MuteDurationMenu { duration in
|
||||||
self.muted_threads.updateMutedThread(event)
|
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||||
let muted = self.muted_threads.isMutedThread(event, keypair: self.keypair)
|
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) {
|
||||||
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||||
|
damus_state.postbox.send(new_mutelist_ev)
|
||||||
|
}
|
||||||
|
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
||||||
isMutedThread = muted
|
isMutedThread = muted
|
||||||
} label: {
|
} label: {
|
||||||
let imageName = isMutedThread ? "mute" : "mute"
|
let imageName = isMutedThread ? "mute" : "mute"
|
||||||
@ -134,15 +125,15 @@ struct MenuItems: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||||
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
if damus_state.keypair.pubkey != target_pubkey && damus_state.keypair.privkey != nil {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
notify(.report(.note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))))
|
notify(.report(.note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))))
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand")
|
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(role: .destructive) {
|
MuteDurationMenu { duration in
|
||||||
notify(.mute(target_pubkey))
|
notify(.mute(.user(target_pubkey, duration?.date_from_now)))
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
|
Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
|
||||||
}
|
}
|
||||||
|
@ -9,20 +9,25 @@ import SwiftUI
|
|||||||
|
|
||||||
/// A container view that shows or hides provided content based on whether the given event should be muted or not, with built-in user controls to show or hide content, and an option to customize the muted box
|
/// 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 {
|
struct EventMutingContainerView<Content: View>: View {
|
||||||
typealias MuteBoxViewClosure = ((_ shown: Binding<Bool>) -> AnyView)
|
typealias MuteBoxViewClosure = ((_ shown: Binding<Bool>, _ mutedReason: MuteItem?) -> AnyView)
|
||||||
|
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let content: Content
|
let content: Content
|
||||||
var customMuteBox: MuteBoxViewClosure?
|
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 shown: Bool
|
||||||
|
|
||||||
|
@State var muted_reason: MuteItem?
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent, @ViewBuilder content: () -> Content) {
|
init(damus_state: DamusState, event: NostrEvent, @ViewBuilder content: () -> Content) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.event = event
|
self.event = event
|
||||||
self.content = content()
|
self.content = content()
|
||||||
self._shown = State(initialValue: should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event))
|
self._shown = State(initialValue: should_show_event(state: damus_state, ev: event))
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent, muteBox: @escaping MuteBoxViewClosure, @ViewBuilder content: () -> Content) {
|
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 {
|
var should_mute: Bool {
|
||||||
return !should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event)
|
return !should_show_event(state: damus_state, ev: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if should_mute {
|
if should_mute {
|
||||||
if let customMuteBox {
|
if let customMuteBox {
|
||||||
customMuteBox($shown)
|
customMuteBox($shown, muted_reason)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
EventMutedBoxView(shown: $shown)
|
EventMutedBoxView(shown: $shown, reason: muted_reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shown {
|
if shown {
|
||||||
@ -49,13 +54,16 @@ struct EventMutingContainerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.new_mutes)) { mutes in
|
.onReceive(handle_notify(.new_mutes)) { mutes in
|
||||||
if mutes.contains(event.pubkey) {
|
let new_muted_event_reason = damus_state.mutelist_manager.event_muted_reason(event)
|
||||||
|
if new_muted_event_reason != nil {
|
||||||
shown = false
|
shown = false
|
||||||
|
muted_reason = new_muted_event_reason
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.new_unmutes)) { unmutes in
|
.onReceive(handle_notify(.new_unmutes)) { unmutes in
|
||||||
if unmutes.contains(event.pubkey) {
|
if damus_state.mutelist_manager.event_muted_reason(event) != nil {
|
||||||
shown = true
|
shown = true
|
||||||
|
muted_reason = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,6 +72,7 @@ struct EventMutingContainerView<Content: View>: View {
|
|||||||
/// A box that instructs the user about a content that has been muted.
|
/// A box that instructs the user about a content that has been muted.
|
||||||
struct EventMutedBoxView: View {
|
struct EventMutedBoxView: View {
|
||||||
@Binding var shown: Bool
|
@Binding var shown: Bool
|
||||||
|
var reason: MuteItem?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -71,9 +80,13 @@ struct EventMutedBoxView: View {
|
|||||||
.foregroundColor(DamusColors.adaptableGrey)
|
.foregroundColor(DamusColors.adaptableGrey)
|
||||||
|
|
||||||
HStack {
|
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()
|
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()
|
shown.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
113
damus/Views/Muting/AddMuteItemView.swift
Normal file
113
damus/Views/Muting/AddMuteItemView.swift
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// AddMuteItemView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/10/24.
|
||||||
|
//
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AddMuteItemView: View {
|
||||||
|
let state: DamusState
|
||||||
|
@State var new_text: String = ""
|
||||||
|
@State var expiration: DamusDuration = .indefinite
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Add mute item", comment: "Title text to indicate user to an add an item to their mutelist.")
|
||||||
|
.font(.system(size: 20, weight: .bold))
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
Picker(selection: $expiration) {
|
||||||
|
ForEach(DamusDuration.allCases, id: \.self) { duration in
|
||||||
|
Text(duration.title).tag(duration)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Duration", comment: "The duration in which to mute the given item.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Label("", image: "copy2")
|
||||||
|
.onTapGesture {
|
||||||
|
if let pasted_text = UIPasteboard.general.string {
|
||||||
|
self.new_text = pasted_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextField(NSLocalizedString("npub, #hashtag, phrase", comment: "Placeholder example for relay server address."), text: $new_text)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
|
||||||
|
Label("", image: "close-circle")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.opacity((new_text == "") ? 0.0 : 1.0)
|
||||||
|
.onTapGesture {
|
||||||
|
self.new_text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(.secondary.opacity(0.2))
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
let expiration_date: Date? = self.expiration.date_from_now
|
||||||
|
let mute_item: MuteItem? = {
|
||||||
|
if new_text.starts(with: "npub") {
|
||||||
|
if let pubkey: Pubkey = bech32_pubkey_decode(new_text) {
|
||||||
|
return .user(pubkey, expiration_date)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if new_text.starts(with: "#") {
|
||||||
|
// Remove the starting `#` character
|
||||||
|
return .hashtag(Hashtag(hashtag: String("\(new_text)".dropFirst())), expiration_date)
|
||||||
|
} else {
|
||||||
|
return .word(new_text, expiration_date)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Actually update & relay the new mute list
|
||||||
|
if let mute_item {
|
||||||
|
let existing_mutelist = state.mutelist_manager.event
|
||||||
|
|
||||||
|
guard
|
||||||
|
let full_keypair = state.keypair.to_full(),
|
||||||
|
let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mutelist_manager.set_mutelist(mutelist)
|
||||||
|
state.postbox.send(mutelist)
|
||||||
|
}
|
||||||
|
|
||||||
|
new_text = ""
|
||||||
|
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(verbatim: "Add mute item")
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddMuteItemView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AddMuteItemView(state: test_damus_state)
|
||||||
|
}
|
||||||
|
}
|
35
damus/Views/Muting/MuteDurationMenu.swift
Normal file
35
damus/Views/Muting/MuteDurationMenu.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// MuteDurationMenu.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/14/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MuteDurationMenu<T: View>: View {
|
||||||
|
var action: (DamusDuration?) -> Void
|
||||||
|
@ViewBuilder var label: () -> T
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(DamusDuration.allCases, id: \.self) { duration in
|
||||||
|
Button {
|
||||||
|
action(duration)
|
||||||
|
} label: {
|
||||||
|
Text("\(duration.title)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
self.label()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
MuteDurationMenu { _ in
|
||||||
|
|
||||||
|
} label: {
|
||||||
|
Text("Mute hashtag")
|
||||||
|
}
|
||||||
|
}
|
@ -9,55 +9,130 @@ import SwiftUI
|
|||||||
|
|
||||||
struct MutelistView: View {
|
struct MutelistView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
@State var users: [Pubkey]
|
@State var show_add_muteitem: Bool = false
|
||||||
|
|
||||||
func RemoveAction(pubkey: Pubkey) -> some View {
|
@State var users: [MuteItem] = []
|
||||||
|
@State var hashtags: [MuteItem] = []
|
||||||
|
@State var threads: [MuteItem] = []
|
||||||
|
@State var words: [MuteItem] = []
|
||||||
|
|
||||||
|
func RemoveAction(item: MuteItem) -> some View {
|
||||||
Button {
|
Button {
|
||||||
guard let mutelist = damus_state.contacts.mutelist,
|
guard let mutelist = damus_state.mutelist_manager.event,
|
||||||
let keypair = damus_state.keypair.to_full(),
|
let keypair = damus_state.keypair.to_full(),
|
||||||
let new_ev = remove_from_mutelist(keypair: keypair,
|
let new_ev = remove_from_mutelist(keypair: keypair,
|
||||||
prev: mutelist,
|
prev: mutelist,
|
||||||
to_remove: .pubkey(pubkey))
|
to_remove: item)
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.contacts.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.postbox.send(new_ev)
|
damus_state.postbox.send(new_ev)
|
||||||
users = get_mutelist_users(new_ev)
|
updateMuteItems()
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||||
}
|
}
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMuteItems() {
|
||||||
|
users = Array(damus_state.mutelist_manager.users)
|
||||||
|
hashtags = Array(damus_state.mutelist_manager.hashtags)
|
||||||
|
threads = Array(damus_state.mutelist_manager.threads)
|
||||||
|
words = Array(damus_state.mutelist_manager.words)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(users, id: \.self) { pubkey in
|
List {
|
||||||
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
Section(NSLocalizedString("Users", comment: "Section header title for a list of muted users.")) {
|
||||||
.id(pubkey)
|
ForEach(users, id: \.self) { user in
|
||||||
.swipeActions {
|
if case let MuteItem.user(pubkey, _) = user {
|
||||||
RemoveAction(pubkey: pubkey)
|
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
||||||
|
.id(pubkey)
|
||||||
|
.swipeActions {
|
||||||
|
RemoveAction(item: .user(pubkey, nil))
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
}
|
||||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) {
|
||||||
|
ForEach(hashtags, id: \.self) { item in
|
||||||
|
if case let MuteItem.hashtag(hashtag, _) = item {
|
||||||
|
Text("#\(hashtag.hashtag)")
|
||||||
|
.id(hashtag.hashtag)
|
||||||
|
.swipeActions {
|
||||||
|
RemoveAction(item: .hashtag(hashtag, nil))
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
damus_state.nav.push(route: Route.Search(search: SearchModel.init(state: damus_state, search: NostrFilter(hashtag: [hashtag.hashtag]))))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) {
|
||||||
|
ForEach(words, id: \.self) { item in
|
||||||
|
if case let MuteItem.word(word, _) = item {
|
||||||
|
Text("\(word)")
|
||||||
|
.id(word)
|
||||||
|
.swipeActions {
|
||||||
|
RemoveAction(item: .word(word, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")) {
|
||||||
|
ForEach(threads, id: \.self) { item in
|
||||||
|
if case let MuteItem.thread(note_id, _) = item {
|
||||||
|
if let event = damus_state.events.lookup(note_id) {
|
||||||
|
EventView(damus: damus_state, event: event)
|
||||||
|
.id(note_id.hex())
|
||||||
|
.swipeActions {
|
||||||
|
RemoveAction(item: .thread(note_id, nil))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
|
.navigationTitle(NSLocalizedString("Muted", comment: "Navigation title of view to see list of muted users & phrases."))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
users = get_mutelist_users(damus_state.contacts.mutelist)
|
updateMuteItems()
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.new_mutes)) { new_mutes in
|
||||||
|
updateMuteItems()
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.new_unmutes)) { new_unmutes in
|
||||||
|
updateMuteItems()
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button {
|
||||||
|
self.show_add_muteitem = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
AddMuteItemView(state: damus_state)
|
||||||
|
.presentationDetents([.height(300)])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
} else {
|
||||||
|
AddMuteItemView(state: damus_state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func get_mutelist_users(_ mutelist: NostrEvent?) -> Array<Pubkey> {
|
|
||||||
guard let mutelist else { return [] }
|
|
||||||
return Array(mutelist.referenced_pubkeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MutelistView_Previews: PreviewProvider {
|
struct MutelistView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MutelistView(damus_state: test_damus_state, users: [test_note.pubkey, test_note.pubkey])
|
MutelistView(damus_state: test_damus_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,25 +179,28 @@ struct ProfileView: View {
|
|||||||
notify(.report(.user(profile.pubkey)))
|
notify(.report(.user(profile.pubkey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if damus_state.contacts.is_muted(profile.pubkey) {
|
if damus_state.mutelist_manager.is_muted(.user(profile.pubkey, nil)) {
|
||||||
Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) {
|
Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) {
|
||||||
guard
|
guard
|
||||||
let keypair = damus_state.keypair.to_full(),
|
let keypair = damus_state.keypair.to_full(),
|
||||||
let mutelist = damus_state.contacts.mutelist
|
let mutelist = damus_state.mutelist_manager.event
|
||||||
else {
|
else {
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.contacts.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.postbox.send(new_ev)
|
damus_state.postbox.send(new_ev)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) {
|
MuteDurationMenu { duration in
|
||||||
notify(.mute(profile.pubkey))
|
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
|
||||||
|
} label: {
|
||||||
|
Text(NSLocalizedString("Mute", comment: "Button to mute a profile."))
|
||||||
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ struct RepostedEvent: View {
|
|||||||
EventMutingContainerView(
|
EventMutingContainerView(
|
||||||
damus_state: damus,
|
damus_state: damus,
|
||||||
event: inner_ev,
|
event: inner_ev,
|
||||||
muteBox: { event_shown in
|
muteBox: { event_shown, muted_reason in
|
||||||
AnyView(
|
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
|
.padding(.horizontal, 5) // Add a bit of horizontal padding to avoid the mute box from touching the edges of the screen
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
|
@ -59,7 +59,8 @@ struct SearchHomeView: View {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) {
|
let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
|
||||||
|
if event_muted {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ struct SearchView: View {
|
|||||||
let appstate: DamusState
|
let appstate: DamusState
|
||||||
@ObservedObject var search: SearchModel
|
@ObservedObject var search: SearchModel
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var is_hashtag_muted: Bool = false
|
||||||
|
|
||||||
var content_filter: (NostrEvent) -> Bool {
|
var content_filter: (NostrEvent) -> Bool {
|
||||||
let filters = ContentFilters.defaults(damus_state: self.appstate)
|
let filters = ContentFilters.defaults(damus_state: self.appstate)
|
||||||
@ -41,7 +42,69 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
search.filter_muted()
|
search.filter_muted()
|
||||||
|
|
||||||
|
if let hashtag_string = search.search.hashtag?.first,
|
||||||
|
notif.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) {
|
||||||
|
is_hashtag_muted = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.new_unmutes)) { unmutes in
|
||||||
|
if let hashtag_string = search.search.hashtag?.first,
|
||||||
|
unmutes.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) {
|
||||||
|
is_hashtag_muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
if let hashtag = search.search.hashtag?.first {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
if is_hashtag_muted {
|
||||||
|
Button {
|
||||||
|
guard
|
||||||
|
let full_keypair = appstate.keypair.to_full(),
|
||||||
|
let existing_mutelist = appstate.mutelist_manager.event,
|
||||||
|
let mutelist = remove_from_mutelist(keypair: full_keypair, prev: existing_mutelist, to_remove: .hashtag(Hashtag(hashtag: hashtag), nil))
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||||
|
appstate.postbox.send(mutelist)
|
||||||
|
} label: {
|
||||||
|
Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MuteDurationMenu { duration in
|
||||||
|
mute_hashtag(hashtag_string: hashtag, expiration_time: duration?.date_from_now)
|
||||||
|
} label: {
|
||||||
|
Text("Mute Hashtag", comment: "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if let hashtag_string = search.search.hashtag?.first {
|
||||||
|
is_hashtag_muted = (appstate.mutelist_manager.event?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mute_hashtag(hashtag_string: String, expiration_time: Date?) {
|
||||||
|
let existing_mutelist = appstate.mutelist_manager.event
|
||||||
|
|
||||||
|
guard
|
||||||
|
let full_keypair = appstate.keypair.to_full(),
|
||||||
|
let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time))
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||||
|
appstate.postbox.send(mutelist)
|
||||||
}
|
}
|
||||||
|
|
||||||
var described_search: DescribedSearch {
|
var described_search: DescribedSearch {
|
||||||
|
@ -66,7 +66,7 @@ struct SideMenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
|
NavigationLink(value: Route.MuteList) {
|
||||||
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
|
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,9 +70,9 @@ struct ThreadView: View {
|
|||||||
EventMutingContainerView(
|
EventMutingContainerView(
|
||||||
damus_state: state,
|
damus_state: state,
|
||||||
event: self.thread.event,
|
event: self.thread.event,
|
||||||
muteBox: { event_shown in
|
muteBox: { event_shown, muted_reason in
|
||||||
AnyView(
|
AnyView(
|
||||||
EventMutedBoxView(shown: event_shown)
|
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,13 @@ final class ListTests: XCTestCase {
|
|||||||
let pubkey = test_keypair_full.pubkey
|
let pubkey = test_keypair_full.pubkey
|
||||||
let to_mute = test_pubkey
|
let to_mute = test_pubkey
|
||||||
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
|
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.pubkey, pubkey)
|
||||||
XCTAssertEqual(mutelist.content, "")
|
XCTAssertEqual(mutelist.content, "")
|
||||||
XCTAssertEqual(mutelist.tags.count, 2)
|
XCTAssertEqual(mutelist.tags.count, 1)
|
||||||
XCTAssertEqual(mutelist.tags[0][0].string(), "d")
|
XCTAssertEqual(mutelist.tags[0][0].string(), "p")
|
||||||
XCTAssertEqual(mutelist.tags[0][1].string(), "mute")
|
XCTAssertEqual(mutelist.tags[0][1].string(), to_mute.hex())
|
||||||
XCTAssertEqual(mutelist.tags[1][0].string(), "p")
|
|
||||||
XCTAssertEqual(mutelist.tags[1][1].string(), to_mute.hex())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateAndRemoveMuteList() throws {
|
func testCreateAndRemoveMuteList() throws {
|
||||||
@ -39,14 +37,12 @@ final class ListTests: XCTestCase {
|
|||||||
let pubkey = test_keypair_full.pubkey
|
let pubkey = test_keypair_full.pubkey
|
||||||
let to_mute = test_pubkey
|
let to_mute = test_pubkey
|
||||||
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
|
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))!
|
||||||
let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(to_mute))!
|
let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(to_mute, nil))!
|
||||||
|
|
||||||
XCTAssertEqual(new.pubkey, pubkey)
|
XCTAssertEqual(new.pubkey, pubkey)
|
||||||
XCTAssertEqual(new.content, "")
|
XCTAssertEqual(new.content, "")
|
||||||
XCTAssertEqual(new.tags.count, 1)
|
XCTAssertEqual(new.tags.count, 0)
|
||||||
XCTAssertEqual(new.tags[0][0].string(), "d")
|
|
||||||
XCTAssertEqual(new.tags[0][1].string(), "mute")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAddToExistingMutelist() throws {
|
func testAddToExistingMutelist() throws {
|
||||||
@ -55,17 +51,25 @@ final class ListTests: XCTestCase {
|
|||||||
let to_mute = test_pubkey
|
let to_mute = test_pubkey
|
||||||
let to_mute_2 = test_pubkey_2
|
let to_mute_2 = test_pubkey_2
|
||||||
let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
|
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))!
|
||||||
let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .pubkey(to_mute_2))!
|
let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .user(to_mute_2, nil))!
|
||||||
|
|
||||||
XCTAssertEqual(new.pubkey, pubkey)
|
XCTAssertEqual(new.pubkey, pubkey)
|
||||||
XCTAssertEqual(new.content, "")
|
XCTAssertEqual(new.content, "")
|
||||||
XCTAssertEqual(new.tags.count, 3)
|
XCTAssertEqual(new.tags.count, 2)
|
||||||
XCTAssertEqual(new.tags[0][0].string(), "d")
|
XCTAssertEqual(new.tags[0][0].string(), "p")
|
||||||
XCTAssertEqual(new.tags[0][1].string(), "mute")
|
|
||||||
XCTAssertEqual(new.tags[1][0].string(), "p")
|
XCTAssertEqual(new.tags[1][0].string(), "p")
|
||||||
XCTAssertEqual(new.tags[1][1].string(), to_mute.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[2][0].string(), "p")
|
XCTAssertEqual([new.tags[0][1].string(), new.tags[1][1].string()].sorted(), [to_mute.hex(), to_mute_2.hex()].sorted())
|
||||||
XCTAssertEqual(new.tags[2][1].string(), to_mute_2.hex())
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ final class LongPostTests: XCTestCase {
|
|||||||
XCTAssertEqual(subid, "subid")
|
XCTAssertEqual(subid, "subid")
|
||||||
XCTAssertTrue(ev.should_show_event)
|
XCTAssertTrue(ev.should_show_event)
|
||||||
XCTAssertTrue(!ev.too_big)
|
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(state: test_damus_state, ev: ev))
|
||||||
XCTAssertTrue(validate_event(ev: ev) == .ok )
|
XCTAssertTrue(validate_event(ev: ev) == .ok )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,11 +25,12 @@ func generate_test_damus_state(
|
|||||||
return profiles
|
return profiles
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let mutelist_manager = MutelistManager()
|
||||||
let damus = DamusState(pool: pool,
|
let damus = DamusState(pool: pool,
|
||||||
keypair: test_keypair,
|
keypair: test_keypair,
|
||||||
likes: .init(our_pubkey: our_pubkey),
|
likes: .init(our_pubkey: our_pubkey),
|
||||||
boosts: .init(our_pubkey: our_pubkey),
|
boosts: .init(our_pubkey: our_pubkey),
|
||||||
contacts: .init(our_pubkey: our_pubkey),
|
contacts: .init(our_pubkey: our_pubkey), mutelist_manager: mutelist_manager,
|
||||||
profiles: profiles,
|
profiles: profiles,
|
||||||
dms: .init(our_pubkey: our_pubkey),
|
dms: .init(our_pubkey: our_pubkey),
|
||||||
previews: .init(),
|
previews: .init(),
|
||||||
@ -44,7 +45,6 @@ func generate_test_damus_state(
|
|||||||
postbox: .init(pool: pool),
|
postbox: .init(pool: pool),
|
||||||
bootstrap_relays: .init(),
|
bootstrap_relays: .init(),
|
||||||
replies: .init(our_pubkey: our_pubkey),
|
replies: .init(our_pubkey: our_pubkey),
|
||||||
muted_threads: .init(keypair: test_keypair),
|
|
||||||
wallet: .init(settings: settings),
|
wallet: .init(settings: settings),
|
||||||
nav: .init(),
|
nav: .init(),
|
||||||
music: .init(onChange: {_ in }),
|
music: .init(onChange: {_ in }),
|
||||||
|
58
damusTests/Models/MuteItemTests.swift
Normal file
58
damusTests/Models/MuteItemTests.swift
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// MuteItemTests.swift
|
||||||
|
// damusTests
|
||||||
|
//
|
||||||
|
// Created by Charlie Fish on 1/14/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import damus
|
||||||
|
|
||||||
|
class MuteItemTests: 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"])
|
||||||
|
}
|
||||||
|
}
|
@ -325,6 +325,10 @@ extension NdbNote {
|
|||||||
References<ReplaceableParam>(tags: self.tags)
|
References<ReplaceableParam>(tags: self.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var referenced_mute_items: References<MuteItem> {
|
||||||
|
References<MuteItem>(tags: self.tags)
|
||||||
|
}
|
||||||
|
|
||||||
public var references: References<RefId> {
|
public var references: References<RefId> {
|
||||||
References<RefId>(tags: self.tags)
|
References<RefId>(tags: self.tags)
|
||||||
}
|
}
|
||||||
@ -342,6 +346,14 @@ extension NdbNote {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maybe_get_content(_ keypair: Keypair) -> String? {
|
||||||
|
if known_kind == .dm {
|
||||||
|
return decrypted(keypair: keypair)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func blocks(_ keypair: Keypair) -> Blocks {
|
func blocks(_ keypair: Keypair) -> Blocks {
|
||||||
return get_blocks(keypair: keypair)
|
return get_blocks(keypair: keypair)
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,22 @@ struct NdbTagElem: Sequence, Hashable, Equatable {
|
|||||||
return id.id
|
return id.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func u64() -> UInt64? {
|
||||||
|
switch self.data() {
|
||||||
|
case .id:
|
||||||
|
return nil
|
||||||
|
case .str(let str):
|
||||||
|
var end_ptr = UnsafeMutablePointer<CChar>(nil as OpaquePointer?)
|
||||||
|
let res = strtoull(str.str, &end_ptr, 10)
|
||||||
|
|
||||||
|
if end_ptr?.pointee == 0 {
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func string() -> String {
|
func string() -> String {
|
||||||
switch self.data() {
|
switch self.data() {
|
||||||
case .id(let id):
|
case .id(let id):
|
||||||
|
Loading…
Reference in New Issue
Block a user