Compare commits

...

14 Commits

Author SHA1 Message Date
Daniel D’Aquino 2c84184dbd Improve UX feedback around notification mode setting
Changing the notification mode setting requires successfully sending or
revoking the device token to the server. As this is an action that might
fail, it is important to have a clear UX feedback in case this fails.

Testing
--------

PASS

Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
strfry-push-notify: d6c2ff289c80e0a90874a7499ed6408394659fc9
Coverage:
1. Checked that push notification mode setting is invisible when experimental push notifications mode is disabled
2. Checked that push notification mode setting is visible when experimental push notifications mode is enabled
3. Checked that switching between push and local notifications sends requests to the server
4. Checked that switching to push notification mode will cause local notifications to be suppressed and push notifications will be sent to the APNS server
5. Checked that switching back to local notification mode will cause local notifications to be displayed, and push notifications will NOT be sent to APNS
6. Checked that if the API server is off, switching from local to push notification modes is not possible and shows an error to the user.
7. Checked that sending APNS payload to Apple's test APNS page will actually deliver the push notification successfully.

Closes: https://github.com/damus-io/damus/issues/1704
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino 901a6fc98f Revoke device token when user switches to local notification mode
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino a0f6bdd8d9 Send device token when switching to push notifications mode
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino 8feb228ea0 Add NIP-98 authentication to push notification client
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino b148fb735e Move device token sending logic to its own component
This commit moves the device token logic to a new
PushNotificationClient, to move complexity from this specific feature
away from damusApp.swift

This commit also slightly improves the handling of device tokens, by
caching it on the client struct even if the user is using local
notifications, so that the device token can be sent to the server immediately after
switching to push notifications mode.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino 0a9bcb6189 Add notification mode setting
This allows the user to switch between local and push notifications

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
Daniel D’Aquino 5a68cfa448 Fallback push notification suppression while we do not have entitlement
We do not have the ability to suppress push notifications yet (we are
waiting to receive the entitlement from Apple)

In the meantime, attempt to fallback gracefully where possible

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-24 16:49:09 -07:00
alltheseas c99aaea598 Add working email hyperlink
Closes: https://github.com/damus-io/damus/issues/2260
Closes: https://github.com/damus-io/damus/prs/2272
Changelog-Changed: Added first aid contact damus support email
Signed-off-by: elsat <npub1zafcms4xya5ap9zr7xxr0jlrtrattwlesytn2s42030lzu0dwlzqpd26k5>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-17 15:38:05 -07:00
Daniel D’Aquino 46185c55d1 Chunk home filters to avoid hitting max filter item limits
When a user is following several accounts, they may get a stale feed
caused by the subscription request being rejected by relays (due to max filter item limits).

This commit implements a fix that gets around the issue by
creating several chunked filters for the home feed event and contact
metadata subscriptions.

This is a short to medium-term practical fix, where we get around the
practical limitations imposed by most relays. In the future we should
work on longer-term solutions, which will likely require protocol improvements

Main Test
---------

Procedure:
1. Login with Elsat's npub (Or some account that follows about 2K people)
2. Check the home feed. There should be fresh notes.

REPRO:
Device: iPhone 15 simulator
iOS: 17.4
Damus: 1.9 (3) (0d9954290a)
Results:
- No fresh notes, most recent post is from several hours ago (Feed is stale)

FIX TEST:
Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
Results:
- Fresh notes appear, most recent post is from a few seconds ago.

Other testing:
--------------

- New automated test passing
- All other automated tests passing
- Tested scrolling down the feed on these conditions:
  - Device: iPhone 13 Mini
  - iOS: 17.4.1
  - Accounts:
    - One with about 160 contacts and 10 relays (Daniel D’Aquino)
    - One with about 1K+ contacts and 9 relays (Freedom Smuggler)
    - One with about 981 contacts and 6 relays (jb55)
    - Elsat's account (2K+ accounts and 8 relays)
  - Result: None of those were stale

Changelog-Fixed: Fix stale feed issue when follow list is too big
Closes: https://github.com/damus-io/damus/issues/2194
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-13 10:56:15 -07:00
William Casarin 52aefc8d64 nip10: simplify and fix reply-to-root bugs
This removes EventRefs alltogether and uses the form we use in Damus
Android.

This simplifies our ThreadReply logic and fixes a reply-to-root bug

Reported-by: NotBiebs <justinbieber@stemstr.app>
Changelog-Fixed: Fix thread bug where a quote isn't picked up as a reply
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-11 09:19:22 -07:00
William Casarin 8dbdff7ff0 1.9 (4)
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-10 13:22:20 -07:00
Daniel D’Aquino 784fb20b4f Fix incorrect Privacy manifest plist key
Xcode entered the key description text instead of the actual key code on
our privacy manifest files.

This commit fixes that key.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Link: 20240509233547.69137-1-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-10 09:22:03 -07:00
William Casarin 0d9954290a 1.9 (3)
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 15:29:38 -07:00
William Casarin 13a7ee82d0 nip10: be nicer to deprecated nip10
to ease the transition, let's not add more than 2 etags or deprecated
nip10 gets confused

Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 15:28:16 -07:00
34 changed files with 563 additions and 558 deletions

View File

@ -40,15 +40,32 @@ class NotificationService: UNNotificationServiceExtension {
return
}
guard should_display_notification(state: state, event: nostr_event) else {
// Don't show notification details that match mute list.
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
if state.mutelist_manager.is_event_muted(nostr_event) {
// We cannot really suppress muted notifications until we have the notification supression entitlement.
// The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
content.sound = UNNotificationSound.default
contentHandler(content)
return
}
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
// We should not display notification for this event. Suppress notification.
contentHandler(UNNotificationContent())
// contentHandler(UNNotificationContent())
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
contentHandler(request.content)
return
}
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
contentHandler(UNNotificationContent())
// contentHandler(UNNotificationContent())
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
contentHandler(request.content)
return
}

View File

@ -16,7 +16,7 @@
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>File Timestamp</string>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>

View File

@ -16,7 +16,7 @@
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>File Timestamp</string>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>

View File

@ -136,7 +136,6 @@
4C363A94282704FA006E126D /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A93282704FA006E126D /* Post.swift */; };
4C363A962827096D006E126D /* PostBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A952827096D006E126D /* PostBlock.swift */; };
4C363A9A28283854006E126D /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9928283854006E126D /* Reply.swift */; };
4C363A9C282838B9006E126D /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; };
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9D2828A822006E126D /* ReplyTests.swift */; };
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9F2828A8DD006E126D /* LikeTests.swift */; };
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
@ -465,6 +464,8 @@
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
D72E12782BEED22500F4F781 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12772BEED22400F4F781 /* Array.swift */; };
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
@ -555,7 +556,6 @@
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; };
D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; };
D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
D7CCFC102B05880F00323D86 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; };
D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D5C9C2A6B2CB40024563C /* AsciiCharacter.swift */; };
@ -615,6 +615,7 @@
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
@ -928,7 +929,6 @@
4C363A93282704FA006E126D /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
4C363A952827096D006E126D /* PostBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBlock.swift; sourceTree = "<group>"; };
4C363A9928283854006E126D /* Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = "<group>"; };
4C363A9B282838B9006E126D /* EventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRef.swift; sourceTree = "<group>"; };
4C363A9D2828A822006E126D /* ReplyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyTests.swift; sourceTree = "<group>"; };
4C363A9F2828A8DD006E126D /* LikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeTests.swift; sourceTree = "<group>"; };
4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
@ -1394,6 +1394,8 @@
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
D72E12772BEED22400F4F781 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
@ -1436,6 +1438,7 @@
D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; };
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
@ -1662,6 +1665,7 @@
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
);
path = Models;
sourceTree = "<group>";
@ -1793,7 +1797,6 @@
4C45E5002BED4CE10025A428 /* NIP10 */ = {
isa = PBXGroup;
children = (
4C363A9B282838B9006E126D /* EventRef.swift */,
4C45E5012BED4D000025A428 /* ThreadReply.swift */,
);
path = NIP10;
@ -2575,6 +2578,7 @@
D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */,
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@ -2699,6 +2703,7 @@
children = (
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
D72E12772BEED22400F4F781 /* Array.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -3255,6 +3260,7 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
D72E12782BEED22500F4F781 /* Array.swift in Sources */,
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
@ -3288,6 +3294,7 @@
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */,
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */,
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
@ -3350,7 +3357,6 @@
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
4C5E54032A9522F600FF6E60 /* UserStatus.swift in Sources */,
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
@ -3555,6 +3561,7 @@
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */,
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */,
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
@ -3606,7 +3613,6 @@
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */,
D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */,
D7CCFC192B058A3F00323D86 /* Block.swift in Sources */,
D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */,
D798D2202B08592000234419 /* NdbTagIterator.swift in Sources */,
@ -3912,7 +3918,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -3979,7 +3985,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;

View File

@ -59,26 +59,24 @@ func parse_note_content(content: NoteContent) -> Blocks {
}
}
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
if tags.count == 0 {
return []
}
/// build a set of indices for each event mention
let mention_indices = build_mention_indices(blocks, type: .e)
/// simpler case with no mentions
if mention_indices.count == 0 {
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
}
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
func interpret_event_refs(tags: TagsSequence) -> ThreadReply? {
// migration is long over, lets just do this to fix tests
return interpret_event_refs_ndb(tags: tags)
}
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
var evrefs: [EventRef] = []
func interpret_event_refs_ndb(tags: TagsSequence) -> ThreadReply? {
if tags.count == 0 {
return nil
}
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
}
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> ThreadReply? {
var first: Bool = true
var root_id: NoteRef? = nil
var reply_id: NoteRef? = nil
var mention: NoteRef? = nil
var any_marker: Bool = false
for ref in ev_tags {
@ -86,47 +84,32 @@ func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [
any_marker = true
switch marker {
case .root: root_id = ref
case .reply: evrefs.append(.reply(ref))
case .mention: evrefs.append(.mention(.noteref(ref)))
case .reply: reply_id = ref
case .mention: mention = ref
}
} else {
if !any_marker && first {
// deprecated form, only activate if we don't have any markers set
} else if !any_marker {
if first {
root_id = ref
first = false
} else if !any_marker {
evrefs.append(.reply(ref))
}
}
}
if let root_id {
if evrefs.count == 0 {
return [.reply_to_root(root_id)]
} else {
evrefs.insert(.thread_id(root_id), at: 0)
}
}
return evrefs
}
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
var mentions: [EventRef] = []
var ev_refs: [NoteRef] = []
var i: Int = 0
for tag in tags {
if let note_id = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
mentions.append(.mention(.noteref(note_id, index: i)))
} else {
ev_refs.append(note_id)
reply_id = ref
}
}
i += 1
}
var replies = interp_event_refs_without_mentions(ev_refs)
replies.append(contentsOf: mentions)
return replies
// If either reply or root_id is blank while the other is not, then this is
// considered reply-to-root. We should always have a root and reply tag, if they
// are equal this is reply-to-root
if reply_id == nil && root_id != nil {
reply_id = root_id
} else if root_id == nil && reply_id != nil {
root_id = reply_id
}
guard let reply_id, let root_id else {
return nil
}
return ThreadReply(root: root_id, reply: reply_id, mention: mention.map { m in .noteref(m) })
}

View File

@ -308,7 +308,7 @@ struct ContentView: View {
active_sheet = .onboardingSuggestions
hasSeenOnboardingSuggestions = true
}
self.appDelegate?.settings = damus_state?.settings
self.appDelegate?.state = damus_state
}
.sheet(item: $active_sheet) { item in
switch item {

View File

@ -16,7 +16,7 @@ enum FilterState : Int {
func filter(ev: NostrEvent) -> Bool {
switch self {
case .posts:
return ev.known_kind == .boost || !ev.is_reply(.empty)
return ev.known_kind == .boost || !ev.is_reply()
case .posts_and_replies:
return true
}

View File

@ -36,6 +36,7 @@ class DamusState: HeadlessDamusState {
let video: VideoController
let ndb: Ndb
var purple: DamusPurple
var push_notification_client: PushNotificationClient
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: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) {
self.pool = pool
@ -68,6 +69,7 @@ class DamusState: HeadlessDamusState {
keypair: keypair
)
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
}
@discardableResult

View File

@ -42,6 +42,10 @@ enum HomeResubFilter {
}
class HomeModel: ContactsDelegate {
// The maximum amount of contacts placed on a home feed subscription filter.
// If the user has more contacts, chunking or other techniques will be used to avoid sending huge filters
let MAX_CONTACTS_ON_FILTER = 500
// Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION
@ -545,7 +549,8 @@ class HomeModel: ContactsDelegate {
notifications_filter.limit = 500
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
let contacts_filter_chunks = contacts_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = get_last_of_kind(relay_id: relay_id)
@ -598,7 +603,7 @@ class HomeModel: ContactsDelegate {
home_filter.authors = friends
home_filter.limit = 500
var home_filters = [home_filter]
var home_filters = home_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
if followed_hashtags.count != 0 {
@ -728,7 +733,7 @@ class HomeModel: ContactsDelegate {
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
notification_status.new_events = notifs
guard should_display_notification(state: damus_state, event: ev),
guard should_display_notification(state: damus_state, event: ev, mode: .local),
let notification_object = generate_local_notification_object(from: ev, state: damus_state)
else {
return

View File

@ -13,7 +13,7 @@ import UIKit
let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60
func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) {
guard should_display_notification(state: state, event: ev) else {
guard should_display_notification(state: state, event: ev, mode: .local) else {
// We should not display notification. Exit.
return
}
@ -25,7 +25,12 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
create_local_notification(profiles: state.profiles, notify: local_notification)
}
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent) -> Bool {
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) -> Bool {
// Do not show notification if it's coming from a mode different from the one selected by our user
guard state.settings.notifications_mode == mode else {
return false
}
if ev.known_kind == nil {
return false
}

View File

@ -0,0 +1,105 @@
//
// PushNotificationClient.swift
// damus
//
// Created by Daniel DAquino on 2024-05-17.
//
import Foundation
struct PushNotificationClient {
let keypair: Keypair
let settings: UserSettingsStore
private(set) var device_token: Data? = nil
mutating func set_device_token(new_device_token: Data) async throws {
self.device_token = new_device_token
if settings.enable_experimental_push_notifications && settings.notifications_mode == .push {
try await self.send_token()
}
}
func send_token() async throws {
guard let device_token else { return }
// Send the device token and pubkey to the server
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
Log.info("Sending device token to server: %s", for: .push_notifications, token)
let pubkey = self.keypair.pubkey
// Send those as JSON to the server
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
// create post request
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
let json_data = try JSONSerialization.data(withJSONObject: json)
let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: json_data,
payload_type: .json,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Sent device token to Damus push notification server successfully", for: .push_notifications)
default:
Log.error("Error in sending device_token to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
}
}
return
}
func revoke_token() async throws {
guard let device_token else { return }
// Send the device token and pubkey to the server
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
Log.info("Revoking device token from server: %s", for: .push_notifications, token)
let pubkey = self.keypair.pubkey
// Send those as JSON to the server
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
// create post request
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_REVOKER_TEST_URL : Constants.DEVICE_TOKEN_REVOKER_PRODUCTION_URL
let json_data = try JSONSerialization.data(withJSONObject: json)
let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: json_data,
payload_type: .json,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Sent device token removal request to Damus push notification server successfully", for: .push_notifications)
default:
Log.error("Error in sending device_token removal to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
}
}
return
}
}
// MARK: Helper structures
extension PushNotificationClient {
enum ClientError: Error {
case http_response_error(status_code: Int, response: Data)
}
}

View File

@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair)
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
{
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
return

View File

@ -60,7 +60,7 @@ class ThreadModel: ObservableObject {
var event_filter = NostrFilter()
var ref_events = NostrFilter()
let thread_id = event.thread_id(keypair: .empty)
let thread_id = event.thread_id()
ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [.text]

View File

@ -155,6 +155,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "like_notification", default_value: true)
var like_notification: Bool
@StringSetting(key: "notifications_mode", default_value: .local)
var notifications_mode: NotificationsMode
@Setting(key: "notification_only_from_following", default_value: false)
var notification_only_from_following: Bool
@ -326,6 +329,36 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "latest_contact_event_id", default_value: nil)
var latest_contact_event_id_hex: String?
// MARK: Helper types
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
var id: String { self.rawValue }
func to_string() -> String {
return rawValue
}
init?(from string: String) {
guard let notifications_mode = NotificationsMode(rawValue: string) else {
return nil
}
self = notifications_mode
}
func text_description() -> String {
switch self {
case .local:
NSLocalizedString("Local", comment: "Option for notification mode setting: Local notification mode")
case .push:
NSLocalizedString("Push", comment: "Option for notification mode setting: Push notification mode")
}
}
case local
case push
}
}
func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {

View File

@ -1,151 +0,0 @@
//
// EventRef.swift
// damus
//
// Created by William Casarin on 2022-05-08.
//
import Foundation
enum EventRef: Equatable {
case mention(Mention<NoteRef>)
case thread_id(NoteRef)
case reply(NoteRef)
case reply_to_root(NoteRef)
var note_ref: NoteRef {
switch self {
case .mention(let mnref): return mnref.ref
case .thread_id(let ref): return ref
case .reply(let ref): return ref
case .reply_to_root(let ref): return ref
}
}
var is_mention: NoteRef? {
if case .mention(let m) = self { return m.ref }
return nil
}
var is_direct_reply: NoteRef? {
switch self {
case .mention:
return nil
case .thread_id:
return nil
case .reply(let refid):
return refid
case .reply_to_root(let refid):
return refid
}
}
var is_thread_id: NoteRef? {
switch self {
case .mention:
return nil
case .thread_id(let referencedId):
return referencedId
case .reply:
return nil
case .reply_to_root(let referencedId):
return referencedId
}
}
var is_reply: NoteRef? {
switch self {
case .mention:
return nil
case .thread_id:
return nil
case .reply(let refid):
return refid
case .reply_to_root(let refid):
return refid
}
}
}
func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
return blocks.reduce(into: []) { acc, block in
switch block {
case .mention(let m):
if m.ref.key == type, let idx = m.index {
acc.insert(idx)
}
case .relay:
return
case .text:
return
case .hashtag:
return
case .url:
return
case .invoice:
return
}
}
}
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
if refs.count == 0 {
return []
}
if refs.count == 1 {
return [.reply_to_root(refs[0])]
}
var evrefs: [EventRef] = []
var first: Bool = true
for ref in refs {
if first {
evrefs.append(.thread_id(ref))
first = false
} else {
evrefs.append(.reply(ref))
}
}
return evrefs
}
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
var mentions: [EventRef] = []
var ev_refs: [NoteRef] = []
var i: Int = 0
for tag in tags {
if let ref = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
let mention = Mention<NoteRef>(index: i, ref: ref)
mentions.append(.mention(mention))
} else {
ev_refs.append(ref)
}
}
i += 1
}
var replies = interp_event_refs_without_mentions(ev_refs)
replies.append(contentsOf: mentions)
return replies
}
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
if tags.count == 0 {
return []
}
/// build a set of indices for each event mention
let mention_indices = build_mention_indices(blocks, type: .e)
/// simpler case with no mentions
if mention_indices.count == 0 {
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
}
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
}

View File

@ -10,54 +10,23 @@ import Foundation
struct ThreadReply {
let root: NoteRef
let reply: NoteRef?
let reply: NoteRef
let mention: Mention<NoteRef>?
var is_reply_to_root: Bool {
guard let reply else {
// if we have no reply and only root then this is reply-to-root,
// but it should never really be in this form...
return true
}
return root.id == reply.id
}
init(root: NoteRef, reply: NoteRef?, mention: Mention<NoteRef>?) {
init(root: NoteRef, reply: NoteRef, mention: Mention<NoteRef>?) {
self.root = root
self.reply = reply
self.mention = mention
}
init?(event_refs: [EventRef]) {
var root: NoteRef? = nil
var reply: NoteRef? = nil
var mention: Mention<NoteRef>? = nil
for evref in event_refs {
switch evref {
case .mention(let m):
mention = m
case .thread_id(let r):
root = r
case .reply(let r):
reply = r
case .reply_to_root(let r):
root = r
reply = r
}
}
// reply with no root should be considered reply-to-root
if root == nil && reply != nil {
root = reply
}
// nip10 threads must have a root
guard let root else {
init?(tags: TagsSequence) {
guard let tr = interpret_event_refs_ndb(tags: tags) else {
return nil
}
self = ThreadReply(root: root, reply: reply, mention: mention)
self = tr
}
}

View File

@ -54,4 +54,68 @@ struct NostrFilter: Codable, Equatable {
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
NostrFilter(hashtag: htags.map { $0.lowercased() })
}
/// Splits the filter on a given filter path/axis into chunked filters
///
/// - Parameter path: The path where chunking should be done
/// - Parameter chunk_size: The maximum size of each chunk.
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
func chunked(on path: ChunkPath, into chunk_size: Int) -> [Self] {
let chunked_slices = self.get_slice(from: path).chunked(into: chunk_size)
var chunked_filters: [NostrFilter] = []
for chunked_slice in chunked_slices {
var chunked_filter = self
chunked_filter.apply_slice(chunked_slice)
chunked_filters.append(chunked_filter)
}
return chunked_filters
}
/// Gets a slice from a NostrFilter on a given path/axis
///
/// - Parameter path: The path where chunking should be done
/// - Parameter chunk_size: The maximum size of each chunk.
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
func get_slice(from path: ChunkPath) -> Slice {
switch path {
case .pubkeys:
return .pubkeys(self.pubkeys)
case .authors:
return .authors(self.authors)
}
}
/// Overrides one member/axis of a NostrFilter using a specific slice
/// - Parameter slice: The slice to be applied on this NostrFilter
mutating func apply_slice(_ slice: Slice) {
switch slice {
case .pubkeys(let pubkeys):
self.pubkeys = pubkeys
case .authors(let authors):
self.authors = authors
}
}
/// A path to one of the axes of a NostrFilter.
enum ChunkPath {
case pubkeys
case authors
// Other paths/axes not supported yet
}
/// Represents the value of a single axis of a NostrFilter
enum Slice {
case pubkeys([Pubkey]?)
case authors([Pubkey]?)
func chunked(into chunk_size: Int) -> [Slice] {
switch self {
case .pubkeys(let array):
return (array ?? []).chunked(into: chunk_size).map({ .pubkeys($0) })
case .authors(let array):
return (array ?? []).chunked(into: chunk_size).map({ .authors($0) })
}
}
}
}

View File

@ -12,6 +12,8 @@ class Constants {
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")!
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info/remove")!
static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")!
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"

View File

@ -169,7 +169,7 @@ class EventCache {
var ev = event
while true {
guard let direct_reply = ev.direct_replies(keypair),
guard let direct_reply = ev.direct_replies(),
let next_ev = lookup(direct_reply), next_ev != ev
else {
break
@ -183,7 +183,7 @@ class EventCache {
}
func add_replies(ev: NostrEvent, keypair: Keypair) {
if let reply = ev.direct_replies(keypair) {
if let reply = ev.direct_replies() {
replies.add(id: reply, reply_id: ev.id)
}
}

View File

@ -0,0 +1,26 @@
//
// Array.swift
// damus
//
// Created by Daniel DAquino on 2024-05-10.
//
import Foundation
extension Array {
/// Splits the array into chunks of the specified size.
/// - Parameter size: The maximum size of each chunk.
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
func chunked(into size: Int) -> [[Element]] {
guard size > 0 else { return [self] }
return stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}
extension Array where Element: Equatable {
mutating func removeAll(equalTo item: Element) {
self.removeAll(where: { $0 == item })
}
}

View File

@ -39,7 +39,7 @@ class ReplyCounter {
counted.insert(event.id)
if let reply = event.direct_replies(keypair) {
if let reply = event.direct_replies() {
if event.pubkey == our_pubkey {
self.our_replies[reply] = event
}

View File

@ -79,7 +79,7 @@ enum Route: Hashable {
case .AppearanceSettings(let settings):
AppearanceSettingsView(damus_state: damusState, settings: settings)
case .NotificationSettings(let settings):
NotificationSettingsView(settings: settings)
NotificationSettingsView(damus_state: damusState, settings: settings)
case .ZapSettings(let settings):
ZapSettingsView(settings: settings)
case .TranslationSettings(let settings):

View File

@ -15,7 +15,7 @@ struct ReplyPart: View {
var body: some View {
Group {
if let reply_ref = event.thread_reply(keypair)?.reply {
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
} else {
EmptyView()

View File

@ -111,7 +111,7 @@ struct MenuItems: View {
if event.known_kind != .dm {
MuteDurationMenu { duration in
if let full_keypair = self.damus_state.keypair.to_full(),
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) {
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
damus_state.postbox.send(new_mutelist_ev)
}

View File

@ -40,7 +40,7 @@ struct SelectedEventView: View {
.minimumScaleFactor(0.75)
.lineLimit(1)
if let reply_ref = event.thread_reply(damus.keypair)?.reply {
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
.padding(.horizontal)
}

View File

@ -616,7 +616,7 @@ private func isAlphanumeric(_ char: Character) -> Bool {
}
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
guard let nip10 = replying_to.thread_reply(keypair) else {
guard let nip10 = replying_to.thread_reply() else {
// we're replying to a post that isn't in a thread,
// just add a single reply-to-root tag
return [["e", replying_to.id.hex(), "", "root"]]
@ -624,16 +624,11 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
// otherwise use the root tag from the parent's nip10 reply and include the note
// that we are replying to's note id.
var tags = [
let tags = [
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
["e", replying_to.id.hex(), "", "reply"]
]
// we also add the parent's nip10 reply tag as an additional e tag for context
if let reply = nip10.reply {
tags.append(["e", reply.note_id.hex(), reply.relay ?? ""])
}
return tags
}

View File

@ -72,7 +72,7 @@ struct FirstAidSettingsView: View {
}
if damus_state.contacts.event != nil {
Text(NSLocalizedString("We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support", comment: "Message indicating that no First Aid actions are available."))
Text("We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)", comment: "Message indicating that no First Aid actions are available.")
}
}
.navigationTitle(NSLocalizedString("First Aid", comment: "Navigation title for first aid settings and tools"))

View File

@ -8,7 +8,9 @@
import SwiftUI
struct NotificationSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var notification_mode_setting_error: String? = nil
@Environment(\.dismiss) var dismiss
@ -24,8 +26,60 @@ struct NotificationSettingsView: View {
})
}
func try_to_set_notifications_mode(new_value: UserSettingsStore.NotificationsMode) {
notification_mode_setting_error = nil
if new_value == .push {
Task {
do {
try await damus_state.push_notification_client.send_token()
settings.notifications_mode = new_value
}
catch {
notification_mode_setting_error = String(format: NSLocalizedString("Error configuring push notifications with the server: %@", comment: "Error label shown when user tries to enable push notifications but something fails"), error.localizedDescription)
}
}
}
else {
Task {
do {
try await damus_state.push_notification_client.revoke_token()
settings.notifications_mode = new_value
}
catch {
notification_mode_setting_error = String(format: NSLocalizedString("Error disabling push notifications with the server: %@", comment: "Error label shown when user tries to disable push notifications but something fails"), error.localizedDescription)
}
}
}
}
var body: some View {
Form {
if settings.enable_experimental_push_notifications {
Section(
header: Text("General", comment: "Section header for general damus notifications user configuration"),
footer: VStack {
if let notification_mode_setting_error {
Text(notification_mode_setting_error)
.foregroundStyle(.damusDangerPrimary)
}
}
) {
Picker(NSLocalizedString("Notifications mode", comment: "Prompt selection of the notification mode (Feature to switch between local notifications (generated from user's own phone) or push notifications (generated by Damus server)."),
selection: Binding(
get: { settings.notifications_mode },
set: { newValue in
self.try_to_set_notifications_mode(new_value: newValue)
}
)
) {
ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
Text(notification_mode.text_description())
.tag(notification_mode.rawValue)
}
}
}
}
Section(header: Text("Local Notifications", comment: "Section header for damus local notifications user configuration")) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification)
.toggleStyle(.switch)
@ -65,6 +119,6 @@ struct NotificationSettingsView: View {
struct NotificationSettings_Previews: PreviewProvider {
static var previews: some View {
NotificationSettingsView(settings: UserSettingsStore())
NotificationSettingsView(damus_state: test_damus_state, settings: UserSettingsStore())
}
}

View File

@ -54,14 +54,12 @@ struct MainView: View {
.onAppear {
orientationTracker.setDeviceMajorAxis()
keypair = get_saved_keypair()
appDelegate.keypair = keypair
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var keypair: Keypair? = nil
var settings: UserSettingsStore? = nil
var state: DamusState? = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
@ -71,51 +69,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Return if this feature is disabled
guard let settings = self.settings else { return }
if !settings.enable_experimental_push_notifications {
guard let state else {
return
}
// Send the device token and pubkey to the server
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Received device token: \(token)")
guard let pubkey = keypair?.pubkey else {
return
Task {
try await state.push_notification_client.set_device_token(new_device_token: deviceToken)
}
// Send those as JSON to the server
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
// create post request
let url = settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
var request = URLRequest(url: url)
request.httpMethod = "POST"
// insert json data to the request
request.httpBody = try? JSONSerialization.data(withJSONObject: json, options: [])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) {
print("Unexpected status code: \(response.statusCode)")
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
}
}
task.resume()
}
// Handle the notification in the foreground state

View File

@ -26,36 +26,55 @@ final class NIP10Tests: XCTestCase {
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func test_root_with_mention_nip10() {
let root_id_hex = "a32d70d331f4bea7a859ac71d85a9b4e0c2d1fa9aaf7237a17f85a6227f52fdb"
let root_id = NoteId(hex: root_id_hex)!
let mention_hex = "e47b7e156acec6881c89a53f1a9e349a982024245e2c398f8a5b4973b7a89ab3"
let mention_id = NoteId(hex: mention_hex)!
let tags =
[["e", root_id_hex,"","root"],
["e", mention_hex,"","mention"],
["p","c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0"],
["p","604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2"],
["p","ffd375eb40eb486656a028edbc83825f58ff0d5c4a1ba22fe7745d284529ed08","","mention"],
["q","e47b7e156acec6881c89a53f1a9e349a982024245e2c398f8a5b4973b7a89ab3"]
]
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let thread = ThreadReply(tags: note.tags)
XCTAssertNotNil(thread)
guard let thread else { return }
XCTAssertEqual(thread.root.note_id, root_id)
XCTAssertEqual(thread.reply.note_id, root_id)
XCTAssertEqual(thread.mention?.ref.note_id, mention_id)
}
func test_new_nip10() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51"
let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53"
let mention_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54"
let tags = [
["e", mention_hex, "", "mention"],
["e", direct_reply_hex, "", "reply"],
["e", root_note_id_hex, "", "root"],
["e", reply_hex, "", "reply"],
["e", "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54", "", "mention"],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let direct_reply_id = NoteId(hex: direct_reply_hex)!
let reply_id = NoteId(hex: reply_hex)!
let mention_id = NoteId(hex: mention_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let tr = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(tr?.root.note_id, root_note_id)
XCTAssertEqual(tr?.reply.note_id, reply_id)
XCTAssertEqual(tr?.mention?.ref.note_id, mention_id)
}
func test_repost_root() {
@ -66,19 +85,9 @@ final class NIP10Tests: XCTestCase {
let mention_id = NoteId(hex: mention_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let tr = note.thread_reply()
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [])
XCTAssertNil(tr)
}
func test_direct_reply_old_nip10() {
@ -90,19 +99,14 @@ final class NIP10Tests: XCTestCase {
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let tr = note.thread_reply()
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(tr.root.note_id, root_note_id)
XCTAssertEqual(tr.reply.note_id, root_note_id)
XCTAssertEqual(tr.is_reply_to_root, true)
}
func test_direct_reply_new_nip10() {
@ -114,24 +118,14 @@ final class NIP10Tests: XCTestCase {
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let tr = note.thread_reply()
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
let nip10 = note.thread_reply(test_keypair)!
XCTAssertEqual(nip10.is_reply_to_root, true)
XCTAssertEqual(nip10.root.note_id, root_note_id)
XCTAssertEqual(nip10.reply!.note_id, root_note_id)
XCTAssertEqual(tr.root.note_id, root_note_id)
XCTAssertEqual(tr.reply.note_id, root_note_id)
XCTAssertNil(tr.mention)
XCTAssertEqual(tr.is_reply_to_root, true)
}
// seen in the wild by the gleasonator
@ -143,13 +137,14 @@ final class NIP10Tests: XCTestCase {
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let thread_reply = ThreadReply(event_refs: refs)!
let tr = note.thread_reply()
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(thread_reply.mention, nil)
XCTAssertEqual(thread_reply.root.note_id, root_note_id)
XCTAssertEqual(thread_reply.reply!.note_id, root_note_id)
XCTAssertEqual(thread_reply.is_reply_to_root, true)
XCTAssertNil(tr.mention)
XCTAssertEqual(tr.root.note_id, root_note_id)
XCTAssertEqual(tr.reply.note_id, root_note_id)
XCTAssertEqual(tr.is_reply_to_root, true)
}
func test_marker_reply() {
@ -184,7 +179,7 @@ final class NIP10Tests: XCTestCase {
let replying_to_hex = "a8dc8b74852d7ad114d5d650b2125459c0cba3c1fdcaaf527e03f24082e11ab3"
let pk = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
let last_reply_hex = "1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e"
//let last_reply_hex = "1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e"
let note = decode_nostr_event_json(json: note_json)!
let reply = build_post(state: test_damus_state, post: .init(string: "hello"), action: .replying_to(note), uploadedMedias: [], pubkeys: [pk] + note.referenced_pubkeys.map({pk in pk}))
let root_hex = "00152d2945459fb394fed2ea95af879c903c4ec42d96327a739fa27c023f20e0"
@ -193,7 +188,6 @@ final class NIP10Tests: XCTestCase {
[
["e", root_hex, "wss://nostr.mutinywallet.com/", "root"],
["e", replying_to_hex, "", "reply"],
["e", last_reply_hex, "wss://relay.nostrplebs.com"],
["p", "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e"],
["p", "6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"],
])
@ -218,16 +212,13 @@ final class NIP10Tests: XCTestCase {
let reply_id = NoteId(hex: reply_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [reply_id])
let tr = note.thread_reply()
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(tr.root.note_id, root_note_id)
XCTAssertEqual(tr.reply.note_id, reply_id)
XCTAssertEqual(tr.is_reply_to_root, false)
}
func test_deprecated_nip10() {
@ -245,19 +236,13 @@ final class NIP10Tests: XCTestCase {
let reply_id = NoteId(hex: reply_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let tr = note.thread_reply()
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(tr.root.note_id, root_note_id)
XCTAssertEqual(tr.reply.note_id, reply_id)
XCTAssertEqual(tr.is_reply_to_root, false)
}

View File

@ -0,0 +1,77 @@
//
// NostrFilterTests.swift
// damusTests
//
// Created by Daniel DAquino on 2024-05-10.
//
import XCTest
@testable import damus
final class NostrFilterTests: XCTestCase {
func testChunkedWithPubKeys() {
// Given a NostrFilter with a list of pubkeys
let test_pubkey_1 = Pubkey(hex: "760f108754eb415561239d4079e71766d87e23f7e71c8e5b00d759e54dd8d082")!
let test_pubkey_2 = Pubkey(hex: "065eab63e939ea2f2f72f2305886b13e5e301302da67b5fe8a18022b278fe872")!
let test_pubkey_3 = Pubkey(hex: "aa146d7c6618ebe993702a74c561f54fc046c8a16e388b828cb2f631a1ed9602")!
let test_pubkey_4 = Pubkey(hex: "2f7108dcd33fb484be3e09cea24a1e96868fbc0842e691ca19db63781801089e")!
let test_pubkey_5 = Pubkey(hex: "1cc7c458e6b565a856d7c3791f4eb5ca5890b1f2433f452ed7a917f9aa0e5250")!
let test_pubkey_6 = Pubkey(hex: "2ee1f46a847b6613c33fd766db1e64c7f727c63774fa3ee952261d2c03b81cf2")!
let test_pubkey_7 = Pubkey(hex: "214664a7ca3236b9dd5f76550d322f390fd70cc12908a2e3ff2cdf50085d4ef2")!
let test_pubkey_8 = Pubkey(hex: "40255b02f3d8ccd6178d50f5ce1c1ac2867b3d919832176957b021c1816fce2f")!
let pubkeys: [Pubkey] = [test_pubkey_1, test_pubkey_2, test_pubkey_3, test_pubkey_4]
let authors: [Pubkey] = [test_pubkey_5, test_pubkey_6, test_pubkey_7, test_pubkey_8]
let filter = NostrFilter(
pubkeys: pubkeys,
authors: authors
)
let chunked_pubkeys_filters_size_2 = filter.chunked(on: .pubkeys, into: 2)
XCTAssertEqual(chunked_pubkeys_filters_size_2.count, 2)
XCTAssertEqual(chunked_pubkeys_filters_size_2[0].pubkeys, [test_pubkey_1, test_pubkey_2])
XCTAssertEqual(chunked_pubkeys_filters_size_2[1].pubkeys, [test_pubkey_3, test_pubkey_4])
XCTAssertEqual(chunked_pubkeys_filters_size_2[0].authors, authors)
XCTAssertEqual(chunked_pubkeys_filters_size_2[1].authors, authors)
let chunked_pubkeys_filters_size_3 = filter.chunked(on: .pubkeys, into: 3)
XCTAssertEqual(chunked_pubkeys_filters_size_3.count, 2)
XCTAssertEqual(chunked_pubkeys_filters_size_3[0].pubkeys, [test_pubkey_1, test_pubkey_2, test_pubkey_3])
XCTAssertEqual(chunked_pubkeys_filters_size_3[1].pubkeys, [test_pubkey_4])
XCTAssertEqual(chunked_pubkeys_filters_size_3[0].authors, authors)
XCTAssertEqual(chunked_pubkeys_filters_size_3[1].authors, authors)
let chunked_pubkeys_filters_size_4 = filter.chunked(on: .pubkeys, into: 4)
XCTAssertEqual(chunked_pubkeys_filters_size_4.count, 1)
XCTAssertEqual(chunked_pubkeys_filters_size_4[0].pubkeys, [test_pubkey_1, test_pubkey_2, test_pubkey_3, test_pubkey_4])
XCTAssertEqual(chunked_pubkeys_filters_size_4[0].authors, authors)
let chunked_pubkeys_filters_size_5 = filter.chunked(on: .pubkeys, into: 5)
XCTAssertEqual(chunked_pubkeys_filters_size_5.count, 1)
XCTAssertEqual(chunked_pubkeys_filters_size_5[0].pubkeys, [test_pubkey_1, test_pubkey_2, test_pubkey_3, test_pubkey_4])
XCTAssertEqual(chunked_pubkeys_filters_size_5[0].authors, authors)
let chunked_authors_filters_size_2 = filter.chunked(on: .authors, into: 2)
XCTAssertEqual(chunked_authors_filters_size_2.count, 2)
XCTAssertEqual(chunked_authors_filters_size_2[0].authors, [test_pubkey_5, test_pubkey_6])
XCTAssertEqual(chunked_authors_filters_size_2[1].authors, [test_pubkey_7, test_pubkey_8])
XCTAssertEqual(chunked_authors_filters_size_2[0].pubkeys, pubkeys)
XCTAssertEqual(chunked_authors_filters_size_2[1].pubkeys, pubkeys)
let chunked_authors_filters_size_3 = filter.chunked(on: .authors, into: 3)
XCTAssertEqual(chunked_authors_filters_size_3.count, 2)
XCTAssertEqual(chunked_authors_filters_size_3[0].authors, [test_pubkey_5, test_pubkey_6, test_pubkey_7])
XCTAssertEqual(chunked_authors_filters_size_3[1].authors, [test_pubkey_8])
XCTAssertEqual(chunked_authors_filters_size_3[0].pubkeys, pubkeys)
XCTAssertEqual(chunked_authors_filters_size_3[1].pubkeys, pubkeys)
let chunked_authors_filters_size_4 = filter.chunked(on: .authors, into: 4)
XCTAssertEqual(chunked_authors_filters_size_4.count, 1)
XCTAssertEqual(chunked_authors_filters_size_4[0].authors, [test_pubkey_5, test_pubkey_6, test_pubkey_7, test_pubkey_8])
XCTAssertEqual(chunked_authors_filters_size_4[0].pubkeys, pubkeys)
let chunked_authors_filters_size_5 = filter.chunked(on: .authors, into: 5)
XCTAssertEqual(chunked_authors_filters_size_5.count, 1)
XCTAssertEqual(chunked_authors_filters_size_5[0].authors, [test_pubkey_5, test_pubkey_6, test_pubkey_7, test_pubkey_8])
XCTAssertEqual(chunked_authors_filters_size_5[0].pubkeys, pubkeys)
}
}

View File

@ -18,24 +18,6 @@ class ReplyTests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testMentionIsntReply() throws {
let evid = NoteId(hex: "4090a9017a2beac3f17795d1aafb80d9f2b9eda97e4738501082ed5c927be014")!
let content = "this is #[0] a mention"
let tags = [evid.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 1)
let ref = event_refs[0]
XCTAssertNil(ref.is_reply)
XCTAssertNil(ref.is_thread_id)
XCTAssertNil(ref.is_direct_reply)
XCTAssertEqual(ref.is_mention, .some(.init(note_id: evid)))
}
func testAtAtEnd() {
let content = "what @"
let blocks = parse_post_blocks(content: content)
@ -70,49 +52,20 @@ class ReplyTests: XCTestCase {
XCTAssertEqual(blocks[2].asHashtag, "nope")
}
func testRootReplyWithMention() throws {
let content = "this is #[1] a mention"
let thread_id = NoteId(hex: "c75e5cbafbefd5de2275f831c2a2386ea05ec5e5a78a5ccf60d467582db48945")!
let mentioned_id = NoteId(hex: "5a534797e8cd3b9f4c1cf63e20e48bd0e8bd7f8c4d6353fbd576df000f6f54d3")!
let tags = [thread_id.tag, mentioned_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
XCTAssertEqual(event_refs.count, 2)
XCTAssertNotNil(event_refs[0].is_reply)
XCTAssertNotNil(event_refs[0].is_thread_id)
XCTAssertNotNil(event_refs[0].is_reply)
XCTAssertNotNil(event_refs[0].is_direct_reply)
XCTAssertEqual(event_refs[0].is_reply, .some(NoteRef(note_id: thread_id)))
XCTAssertEqual(event_refs[0].is_thread_id, .some(NoteRef(note_id: thread_id)))
XCTAssertNotNil(event_refs[1].is_mention)
XCTAssertEqual(event_refs[1].is_mention, .some(NoteRef(note_id: mentioned_id)))
}
func testEmptyMention() throws {
let content = "this is some & content"
let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let post_blocks = parse_post_blocks(content: content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: [])
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
let tr = interpret_event_refs(tags: ev.tags)
XCTAssertEqual(event_refs.count, 0)
XCTAssertNil(tr)
XCTAssertEqual(post_tags.blocks.count, 1)
XCTAssertEqual(post_tags.tags.count, 0)
XCTAssertEqual(post_blocks.count, 1)
}
func testManyMentions() throws {
let content = "#[10]"
let tags: [[String]] = [[],[],[],[],[],[],[],[],[],[],["p", "3e999f94e2cb34ef44a64b351141ac4e51b5121b2d31aed4a6c84602a1144692"]]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let mentions = blocks.filter { $0.asMention != nil }
XCTAssertEqual(mentions.count, 1)
}
func testNewlineMentions() throws {
let bech32_pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
let pk = bech32_pubkey_decode(bech32_pk)!
@ -145,17 +98,12 @@ class ReplyTests: XCTestCase {
let reply_id = NoteId(hex: "80093e9bdb495728f54cda2bad4aed096877189552b3d41264e73b9a9595be22")!
let tags = [thread_id.tag, reply_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
let tr = interpret_event_refs(tags: ev.tags)
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(event_refs.count, 2)
let r1 = event_refs[0]
let r2 = event_refs[1]
XCTAssertEqual(r1.is_thread_id, .some(.note_id(thread_id)))
XCTAssertEqual(r2.is_reply, .some(.note_id(reply_id)))
XCTAssertEqual(r2.is_direct_reply, .some(.note_id(reply_id)))
XCTAssertNil(r1.is_direct_reply)
XCTAssertEqual(tr.root.note_id, thread_id)
XCTAssertEqual(tr.reply.note_id, reply_id)
}
func testRootReply() throws {
@ -163,16 +111,14 @@ class ReplyTests: XCTestCase {
let thread_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")!
let tags = [thread_id.tag]
let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)!
let blocks = parse_note_content(content: .content(ev.content,nil)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags)
let tr = interpret_event_refs(tags: ev.tags)
XCTAssertEqual(event_refs.count, 1)
let r = event_refs[0]
XCTAssertEqual(r.is_direct_reply, .some(.note_id(thread_id)))
XCTAssertEqual(r.is_reply, .some(.note_id(thread_id)))
XCTAssertEqual(r.is_thread_id, .some(.note_id(thread_id)))
XCTAssertNil(r.is_mention)
XCTAssertNotNil(tr)
guard let tr else { return }
XCTAssertEqual(tr.root.note_id, thread_id)
XCTAssertEqual(tr.reply.note_id, thread_id)
XCTAssertNil(tr.mention)
}
func testAdjacentComposedMention() throws {
@ -262,28 +208,6 @@ class ReplyTests: XCTestCase {
XCTAssertEqual(new_post.string, "cc @jb55 ")
}
func testNoReply() throws {
let content = "this is a #[0] reply"
let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
let event_refs = interpret_event_refs(blocks: blocks, tags:ev.tags)
XCTAssertEqual(event_refs.count, 0)
}
func testParseMention() throws {
let note_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")!
let tags = [note_id.tag]
let ev = NostrEvent(content: "this is #[0] a mention", keypair: test_keypair, tags: tags)!
let parsed = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].asText, "this is ")
XCTAssertNotNil(parsed[1].asMention)
XCTAssertEqual(parsed[2].asText, " a mention")
}
func testEmptyPostReference() throws {
let parsed = parse_post_blocks(content: "")
XCTAssertEqual(parsed.count, 0)
@ -442,14 +366,4 @@ class ReplyTests: XCTestCase {
XCTAssertEqual(t2, " event mention")
}
func testParseInvalidMention() throws {
let parsed = parse_note_content(content: .content("this is #[0] a mention",nil)).blocks
XCTAssertNotNil(parsed)
XCTAssertEqual(parsed.count, 3)
XCTAssertEqual(parsed[0].asText, "this is ")
XCTAssertEqual(parsed[1].asText, "#[0]")
XCTAssertEqual(parsed[2].asText, " a mention")
}
}

View File

@ -340,8 +340,8 @@ extension NdbNote {
References<RefId>(tags: self.tags)
}
func thread_reply(_ keypair: Keypair) -> ThreadReply? {
ThreadReply(event_refs: interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags))
func thread_reply() -> ThreadReply? {
ThreadReply(tags: self.tags)
}
func get_content(_ keypair: Keypair) -> String {
@ -387,13 +387,13 @@ extension NdbNote {
return dec
}
public func direct_replies(_ keypair: Keypair) -> NoteId? {
return thread_reply(keypair)?.reply?.note_id
public func direct_replies() -> NoteId? {
return thread_reply()?.reply.note_id
}
// NDBTODO: just use Id
public func thread_id(keypair: Keypair) -> NoteId {
guard let root = self.thread_reply(keypair)?.root else {
public func thread_id() -> NoteId {
guard let root = self.thread_reply()?.root else {
return self.id
}
@ -421,8 +421,8 @@ extension NdbNote {
}
*/
func is_reply(_ keypair: Keypair) -> Bool {
return thread_reply(keypair)?.reply != nil
func is_reply() -> Bool {
return thread_reply() != nil
}
func note_language(_ keypair: Keypair) -> String? {

View File

@ -202,52 +202,6 @@ final class NdbTests: XCTestCase {
return opts
}
func test_perf_interp_evrefs_old() {
guard let event = decode_nostr_event_json(test_reply_json) else {
return
}
self.measure(options: longer_iter()) {
let blocks = event.blocks(test_keypair).blocks
let xs = interpret_event_refs(blocks: blocks, tags: event.tags)
XCTAssertEqual(xs.count, 1)
}
}
func test_perf_interp_evrefs_ndb() {
guard let note = NdbNote.owned_from_json(json: test_reply_json) else {
return
}
self.measure(options: longer_iter()) {
let blocks = note.blocks(test_keypair).blocks
let xs = interpret_event_refs_ndb(blocks: blocks, tags: note.tags)
XCTAssertEqual(xs.count, 1)
}
}
func test_decoded_events_are_equal() {
let event = decode_nostr_event_json(test_reply_json)
let note = NdbNote.owned_from_json(json: test_reply_json)
XCTAssertNotNil(note)
XCTAssertNotNil(event)
guard let note else { return }
guard let event else { return }
XCTAssertEqual(note.content_len, UInt32(event.content.utf8.count))
XCTAssertEqual(note.pubkey, event.pubkey)
XCTAssertEqual(note.id, event.id)
let ev_blocks = event.blocks(test_keypair)
let note_blocks = note.blocks(test_keypair)
XCTAssertEqual(ev_blocks, note_blocks)
let event_refs = interpret_event_refs(blocks: ev_blocks.blocks, tags: event.tags)
let note_refs = interpret_event_refs_ndb(blocks: note_blocks.blocks, tags: note.tags)
XCTAssertEqual(event_refs, note_refs)
}
func test_iteration_perf() throws {
guard let note = NdbNote.owned_from_json(json: test_contact_list_json) else {
XCTAssert(false)