Compare commits

..

10 Commits

Author SHA1 Message Date
tyiu e5c82ec64b WIP pinned notes 2025-08-17 17:36:15 -07:00
tyiu fdbf271432 Add relay count and relay view to events
Changelog-Added: Added relay count and relay view to events

Closes: https://github.com/damus-io/damus/issues/1029
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 16:45:49 -07:00
tyiu b26eedc633 Fix note content rendering to not remove whitespace before hashtag
Changelog-Fixed: Fixed note content rendering to not remove whitespace before hashtag

Closes: https://github.com/damus-io/damus/issues/3122
Fixes: f436291209 ("Fix note content rendering to not remove whitespace before hashtag")
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-21 16:37:36 -07:00
tyiu 793970beaf Add relay hints to tags and identifiers
Changelog-Added: Add relay hints to tags and identifiers
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-07-18 15:51:25 -07:00
transifex-integration[bot] 049d9170be Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-07-18 14:52:31 -07:00
transifex-integration[bot] fd10c5672a Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-07-18 14:52:31 -07:00
transifex-integration[bot] 37bd9447f0 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-07-18 14:52:31 -07:00
transifex-integration[bot] e8457d7486 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-07-18 14:52:31 -07:00
transifex-integration[bot] 280297ad35 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-07-18 14:52:31 -07:00
transifex-integration[bot] 7da3ead01e Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-07-18 14:52:31 -07:00
45 changed files with 391 additions and 446 deletions
@@ -26,7 +26,7 @@ struct NotificationFormatter {
content.title = NSLocalizedString("Someone posted a note", comment: "Title label for push notification where someone posted a note") content.title = NSLocalizedString("Someone posted a note", comment: "Title label for push notification where someone posted a note")
content.body = event.content content.body = event.content
break break
case .deprecated_dm: case .dm:
content.title = NSLocalizedString("New message", comment: "Title label for push notifications where a direct message was sent to the user") content.title = NSLocalizedString("New message", comment: "Title label for push notifications where a direct message was sent to the user")
content.body = NSLocalizedString("(Contents are encrypted)", comment: "Label on push notification indicating that the contents of the message are encrypted") content.body = NSLocalizedString("(Contents are encrypted)", comment: "Label on push notification indicating that the contents of the message are encrypted")
break break
+19 -27
View File
@@ -12,14 +12,14 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; }; 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A04DA252E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA212E1F40AC00449A0B /* NIP17DirectMessage.swift */; };
3A04DA262E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA232E1F40AC00449A0B /* NIP59GiftWrap.swift */; };
3A04DA272E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA212E1F40AC00449A0B /* NIP17DirectMessage.swift */; };
3A04DA282E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA232E1F40AC00449A0B /* NIP59GiftWrap.swift */; };
3A04DA292E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA212E1F40AC00449A0B /* NIP17DirectMessage.swift */; };
3A04DA2A2E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04DA232E1F40AC00449A0B /* NIP59GiftWrap.swift */; };
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; }; 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; }; 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A28D3A12E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
3A28D3A22E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
3A28D3A32E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
3A28D3A62E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
3A28D3A72E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
3A28D3A82E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; }; 3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; }; 3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; }; 3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
@@ -1868,8 +1868,6 @@
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = "<group>"; }; 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = "<group>"; };
3169CAEC294FCCFC00EE4006 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = damus/Util/Constants.swift; sourceTree = SOURCE_ROOT; }; 3169CAEC294FCCFC00EE4006 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = damus/Util/Constants.swift; sourceTree = SOURCE_ROOT; };
31D2E846295218AF006D67F8 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; }; 31D2E846295218AF006D67F8 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; };
3A04DA212E1F40AC00449A0B /* NIP17DirectMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP17DirectMessage.swift; sourceTree = "<group>"; };
3A04DA232E1F40AC00449A0B /* NIP59GiftWrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP59GiftWrap.swift; sourceTree = "<group>"; };
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -1877,6 +1875,8 @@
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventView.swift; sourceTree = "<group>"; };
3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedHeaderView.swift; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; }; 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; };
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; }; 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
@@ -2792,20 +2792,13 @@
path = "Empty Views"; path = "Empty Views";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3A04DA222E1F40AC00449A0B /* NIP17 */ = { 3A28D3A42E2F4053003C6F82 /* Pinned */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3A04DA212E1F40AC00449A0B /* NIP17DirectMessage.swift */, 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */,
3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */,
); );
path = NIP17; path = Pinned;
sourceTree = "<group>";
};
3A04DA242E1F40AC00449A0B /* NIP59 */ = {
isa = PBXGroup;
children = (
3A04DA232E1F40AC00449A0B /* NIP59GiftWrap.swift */,
);
path = NIP59;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3A515C4E2DF4E0E6002D3B34 /* Tips */ = { 3A515C4E2DF4E0E6002D3B34 /* Tips */ = {
@@ -3709,6 +3702,7 @@
4CC7AAEE297F11B300430951 /* Events */ = { 4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3A28D3A42E2F4053003C6F82 /* Pinned */,
5C4FA7FA2DC29C3800CE658C /* FollowPack */, 5C4FA7FA2DC29C3800CE658C /* FollowPack */,
5CC852A02BDED9970039FFC5 /* Highlight */, 5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */, 4CA927682A290F8F0098A105 /* Components */,
@@ -3818,8 +3812,6 @@
4CE6DEE527F7A08100C66700 /* damus */ = { 4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3A04DA222E1F40AC00449A0B /* NIP17 */,
3A04DA242E1F40AC00449A0B /* NIP59 */,
D76BE18A2E0CF3BF004AD0C6 /* DIP06 */, D76BE18A2E0CF3BF004AD0C6 /* DIP06 */,
D71527FD2E0A3D5800C893D6 /* NIP51 */, D71527FD2E0A3D5800C893D6 /* NIP51 */,
D7DB93082D69478400DA1EE5 /* NIP65 */, D7DB93082D69478400DA1EE5 /* NIP65 */,
@@ -4724,6 +4716,7 @@
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
3A28D3A32E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */, 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */, 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */, D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
@@ -5028,6 +5021,7 @@
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */, 4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
3A28D3A62E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */, D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */, D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
@@ -5098,8 +5092,6 @@
4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */, 4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */,
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */, 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
7527271E2A93FF0100214108 /* Block.swift in Sources */, 7527271E2A93FF0100214108 /* Block.swift in Sources */,
3A04DA252E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */,
3A04DA262E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */, 4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4C32B9592A9AD44700DC3548 /* Table.swift in Sources */, 4C32B9592A9AD44700DC3548 /* Table.swift in Sources */,
@@ -5262,8 +5254,6 @@
82D6FAAC2CD99F7900C925F4 /* FlatBufferBuilder.swift in Sources */, 82D6FAAC2CD99F7900C925F4 /* FlatBufferBuilder.swift in Sources */,
82D6FAAD2CD99F7900C925F4 /* FlatbuffersErrors.swift in Sources */, 82D6FAAD2CD99F7900C925F4 /* FlatbuffersErrors.swift in Sources */,
82D6FAAE2CD99F7900C925F4 /* Verifier.swift in Sources */, 82D6FAAE2CD99F7900C925F4 /* Verifier.swift in Sources */,
3A04DA292E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */,
3A04DA2A2E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */,
82D6FAAF2CD99F7900C925F4 /* ByteBuffer.swift in Sources */, 82D6FAAF2CD99F7900C925F4 /* ByteBuffer.swift in Sources */,
82D6FAB02CD99F7900C925F4 /* TableVerifier.swift in Sources */, 82D6FAB02CD99F7900C925F4 /* TableVerifier.swift in Sources */,
82D6FAB12CD99F7900C925F4 /* Root.swift in Sources */, 82D6FAB12CD99F7900C925F4 /* Root.swift in Sources */,
@@ -5362,6 +5352,7 @@
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */, 82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */, 82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */, 82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
3A28D3A22E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
82D6FB042CD99F7900C925F4 /* SwipeToDismiss.swift in Sources */, 82D6FB042CD99F7900C925F4 /* SwipeToDismiss.swift in Sources */,
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */, 82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */, 82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
@@ -5520,6 +5511,7 @@
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */, D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */, 82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */, 82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */,
3A28D3A72E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */, 82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */,
82D6FB982CD99F7900C925F4 /* DraftsModel.swift in Sources */, 82D6FB982CD99F7900C925F4 /* DraftsModel.swift in Sources */,
82D6FB992CD99F7900C925F4 /* NotificationsModel.swift in Sources */, 82D6FB992CD99F7900C925F4 /* NotificationsModel.swift in Sources */,
@@ -5784,6 +5776,7 @@
D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */, D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */,
D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */, D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */,
D73E5E292C6A97F4007EB227 /* LogoutNotify.swift in Sources */, D73E5E292C6A97F4007EB227 /* LogoutNotify.swift in Sources */,
3A28D3A12E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
D73E5E2A2C6A97F4007EB227 /* OnlyZapsNotify.swift in Sources */, D73E5E2A2C6A97F4007EB227 /* OnlyZapsNotify.swift in Sources */,
D73E5E2B2C6A97F4007EB227 /* PostNotify.swift in Sources */, D73E5E2B2C6A97F4007EB227 /* PostNotify.swift in Sources */,
D73E5E2C2C6A97F4007EB227 /* PresentSheetNotify.swift in Sources */, D73E5E2C2C6A97F4007EB227 /* PresentSheetNotify.swift in Sources */,
@@ -5806,6 +5799,7 @@
D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */, D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */,
D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */, D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */,
D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */, D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */,
3A28D3A82E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */, D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */,
D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */, D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */,
D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */, D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */,
@@ -6246,8 +6240,6 @@
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */, D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */,
D703D76A2C670B2C00A400EA /* Bech32Object.swift in Sources */, D703D76A2C670B2C00A400EA /* Bech32Object.swift in Sources */,
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */, D73E5E162C6A9619007EB227 /* PostView.swift in Sources */,
3A04DA272E1F40AC00449A0B /* NIP17DirectMessage.swift in Sources */,
3A04DA282E1F40AC00449A0B /* NIP59GiftWrap.swift in Sources */,
D703D7872C670C7E00A400EA /* DamusPurpleEnvironment.swift in Sources */, D703D7872C670C7E00A400EA /* DamusPurpleEnvironment.swift in Sources */,
D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */, D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */,
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */, D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */,
+1 -1
View File
@@ -12,7 +12,7 @@ enum NoteContent {
case content(String, TagsSequence?) case content(String, TagsSequence?)
init(note: NostrEvent, keypair: Keypair) { init(note: NostrEvent, keypair: Keypair) {
if note.known_kind == .deprecated_dm || note.known_kind == .highlight { if note.known_kind == .dm || note.known_kind == .highlight {
self = .content(note.get_content(keypair), note.tags) self = .content(note.get_content(keypair), note.tags)
} else { } else {
self = .note(note) self = .note(note)
+6 -3
View File
@@ -25,12 +25,13 @@ class ActionBarModel: ObservableObject {
@Published private(set) var zaps: Int @Published private(set) var zaps: Int
@Published var zap_total: Int64 @Published var zap_total: Int64
@Published var replies: Int @Published var replies: Int
@Published var relays: Int
static func empty() -> ActionBarModel { static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
} }
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) { init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0, relays: Int = 0) {
self.likes = likes self.likes = likes
self.boosts = boosts self.boosts = boosts
self.zaps = zaps self.zaps = zaps
@@ -42,6 +43,7 @@ class ActionBarModel: ObservableObject {
self.our_reply = our_reply self.our_reply = our_reply
self.our_quote_repost = our_quote_repost self.our_quote_repost = our_quote_repost
self.quote_reposts = quote_reposts self.quote_reposts = quote_reposts
self.relays = relays
} }
func update(damus: DamusState, evid: NoteId) { func update(damus: DamusState, evid: NoteId) {
@@ -56,11 +58,12 @@ class ActionBarModel: ObservableObject {
self.our_zap = damus.zaps.our_zaps[evid]?.first self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid) self.our_reply = damus.replies.our_reply(evid)
self.our_quote_repost = damus.quote_reposts.our_events[evid] self.our_quote_repost = damus.quote_reposts.our_events[evid]
self.relays = (damus.nostrNetwork.pool.seen[evid] ?? []).count
self.objectWillChange.send() self.objectWillChange.send()
} }
var is_empty: Bool { var is_empty: Bool {
return likes == 0 && boosts == 0 && zaps == 0 return likes == 0 && boosts == 0 && zaps == 0 && quote_reposts == 0 && relays == 0
} }
var liked: Bool { var liked: Bool {
+5 -23
View File
@@ -205,7 +205,7 @@ class HomeModel: ContactsDelegate {
handle_boost_event(sub_id: sub_id, ev) handle_boost_event(sub_id: sub_id, ev)
case .like: case .like:
handle_like_event(ev) handle_like_event(ev)
case .deprecated_dm: case .dm:
handle_dm(ev) handle_dm(ev)
case .delete: case .delete:
handle_delete_event(ev) handle_delete_event(ev)
@@ -231,13 +231,8 @@ class HomeModel: ContactsDelegate {
break break
case .interest_list: case .interest_list:
break // Don't care for now break // Don't care for now
case .dm: case .pinned_notes:
break // We should never receive a kind 14 DM. It will always be sealed (kind 13) and then gift wrapped (kind 1059). break // FIXME(tyiu)
case .seal:
break // We should never receive a kind 13 seal. It will always be gift wrapped (kind 1059)
case .gift_wrap:
handle_gift_wrap(ev)
break
} }
} }
@@ -570,9 +565,9 @@ class HomeModel: ContactsDelegate {
var our_blocklist_filter = NostrFilter(kinds: [.mute_list]) 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: [.deprecated_dm, .gift_wrap]) var dms_filter = NostrFilter(kinds: [.dm])
var our_dms_filter = NostrFilter(kinds: [.deprecated_dm]) var our_dms_filter = NostrFilter(kinds: [.dm])
// friends only?... // friends only?...
//dms_filter.authors = friends //dms_filter.authors = friends
@@ -821,19 +816,6 @@ class HomeModel: ContactsDelegate {
self.incoming_dms = [] self.incoming_dms = []
} }
} }
func handle_gift_wrap(_ ev: NostrEvent) {
guard ev.known_kind == .gift_wrap,
let privateKey = damus_state.keypair.privkey else {
return
}
guard let rumor = try? NIP59GiftWrap.unsealedRumor(giftWrapEvent: ev, using: privateKey) else {
return
}
handle_dm(rumor)
}
} }
+28 -3
View File
@@ -64,10 +64,35 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
switch self { switch self {
case .pubkey(let pubkey): return ["p", pubkey.hex()] case .pubkey(let pubkey): return ["p", pubkey.hex()]
case .note(let noteId): return ["e", noteId.hex()] case .note(let noteId): return ["e", noteId.hex()]
case .nevent(let nevent): return ["e", nevent.noteid.hex()] case .nevent(let nevent):
case .nprofile(let nprofile): return ["p", nprofile.author.hex()] var tagBuilder = ["e", nevent.noteid.hex()]
let relay = nevent.relays.first
if let author = nevent.author?.hex() {
tagBuilder.append(relay ?? "")
tagBuilder.append(author)
} else if let relay {
tagBuilder.append(relay)
}
return tagBuilder
case .nprofile(let nprofile):
var tagBuilder = ["p", nprofile.author.hex()]
if let relay = nprofile.relays.first {
tagBuilder.append(relay)
}
return tagBuilder
case .nrelay(let url): return ["r", url] case .nrelay(let url): return ["r", url]
case .naddr(let naddr): return ["a", naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier.string()] case .naddr(let naddr):
var tagBuilder = ["a", "\(naddr.kind.description):\(naddr.author.hex()):\(naddr.identifier.string())"]
if let relay = naddr.relays.first {
tagBuilder.append(relay)
}
return tagBuilder
} }
} }
@@ -51,6 +51,16 @@ class NostrNetworkManager {
func connect() { func connect() {
self.userRelayList.connect() self.userRelayList.connect()
} }
func relaysForEvent(event: NostrEvent) -> [RelayURL] {
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
// and reliability of relays to maximize chances of others finding this event.
if let relays = pool.seen[event.id] {
return Array(relays)
}
return []
}
} }
+14 -1
View File
@@ -76,6 +76,11 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true)) return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
} }
// FIXME(tyiu): There are a lot of hacks to get this function to render the blocks correctly.
// However, the entire note content rendering logic just needs to be rewritten.
// Block previews should actually be rendered in the position of the note content where it was found.
// Currently, we put some previews at the bottom of the note, which is incorrect as they take things out of
// the author's intended context.
func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated { func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
var invoices: [Invoice] = [] var invoices: [Invoice] = []
var urls: [UrlType] = [] var urls: [UrlType] = []
@@ -120,6 +125,7 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
// We should hide whitespace at the end sequence. // We should hide whitespace at the end sequence.
hide_text_index = i hide_text_index = i
} else if case .hashtag = block { } else if case .hashtag = block {
// SPECIAL CASE:
// We should keep hashtags at the end sequence but hide all the other previewables around it. // We should keep hashtags at the end sequence but hide all the other previewables around it.
hide_text_index = i hide_text_index = i
} else { } else {
@@ -171,7 +177,14 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
case .mention(let m): case .mention(let m):
return str + mention_str(m, profiles: profiles) return str + mention_str(m, profiles: profiles)
case .text(let txt): case .text(let txt):
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt)) if case .hashtag = blocks[safe: ind+1] {
// SPECIAL CASE:
// Do not trim whitespaces from suffix if the following block is a hashtag.
// This is because of the code further up (see "SPECIAL CASE").
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: -1, txt: txt))
} else {
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
}
case .relay(let relay): case .relay(let relay):
return str + CompatibleText(stringLiteral: relay) return str + CompatibleText(stringLiteral: relay)
case .hashtag(let htag): case .hashtag(let htag):
+1 -1
View File
@@ -113,7 +113,7 @@ func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamu
return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "") return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "")
} }
} }
else if type == .deprecated_dm, else if type == .dm,
state.settings.dm_notification { state.settings.dm_notification {
let convo = ev.decrypted(keypair: state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message") let convo = ev.decrypted(keypair: state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
return LocalNotification(type: .dm, event: ev, target: .note(ev), content: convo) return LocalNotification(type: .dm, event: ev, target: .note(ev), content: convo)
+61 -16
View File
@@ -23,9 +23,16 @@ class ProfileModel: ObservableObject, Equatable {
return nil return nil
} }
private let MAX_SHARE_RELAYS = 4 @Published var pinned_notes_list: NostrEvent? = nil
var pinned_note_ids: Set<NoteId> {
if let pinned_notes_list {
return Set(pinned_notes_list.referenced_noterefs.map { $0.note_id })
}
return []
}
var events: EventHolder var events: EventHolder
var pinned_events: EventHolder
let pubkey: Pubkey let pubkey: Pubkey
let damus: DamusState let damus: DamusState
@@ -34,6 +41,7 @@ class ProfileModel: ObservableObject, Equatable {
var prof_subid = UUID().description var prof_subid = UUID().description
var conversations_subid = UUID().description var conversations_subid = UUID().description
var findRelay_subid = UUID().description var findRelay_subid = UUID().description
var pinned_subid = UUID().description
var conversation_events: Set<NoteId> = Set() var conversation_events: Set<NoteId> = Set()
init(pubkey: Pubkey, damus: DamusState) { init(pubkey: Pubkey, damus: DamusState) {
@@ -42,6 +50,9 @@ class ProfileModel: ObservableObject, Equatable {
self.events = EventHolder(on_queue: { ev in self.events = EventHolder(on_queue: { ev in
preload_events(state: damus, events: [ev]) preload_events(state: damus, events: [ev])
}) })
self.pinned_events = EventHolder(on_queue: { ev in
preload_events(state: damus, events: [ev])
})
} }
func follows(pubkey: Pubkey) -> Bool { func follows(pubkey: Pubkey) -> Bool {
@@ -76,20 +87,18 @@ class ProfileModel: ObservableObject, Equatable {
} }
} }
func subscribe() { let textKinds: [NostrKind] = [.text, .longform, .highlight]
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
profile_filter.authors = [pubkey] func subscribe() {
let text_filter = NostrFilter(kinds: textKinds, limit: 500, authors: [pubkey])
text_filter.authors = [pubkey] let profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost], authors: [pubkey])
text_filter.limit = 500 let relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
let pinned_notes_filter = NostrFilter(kinds: [.pinned_notes], authors: [pubkey])
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)") print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) //print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event) damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter, pinned_notes_filter], handler: handle_event)
subscribe_to_conversations() subscribe_to_conversations()
} }
@@ -100,7 +109,7 @@ class ProfileModel: ObservableObject, Equatable {
return return
} }
let conversation_kinds: [NostrKind] = [.text, .longform, .highlight] let conversation_kinds: [NostrKind] = textKinds
let limit: UInt32 = 500 let limit: UInt32 = 500
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey]) let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey]) let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
@@ -108,6 +117,15 @@ class ProfileModel: ObservableObject, Equatable {
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event) damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
} }
private func subscribe_to_pinned_notes() {
guard let pinned_notes_list, pinned_notes_list.referenced_noterefs.first != nil else {
return
}
let pinned_filter = NostrFilter(ids: Array(pinned_note_ids), kinds: [.text], authors: [pubkey])
damus.nostrNetwork.pool.subscribe(sub_id: pinned_subid, filters: [pinned_filter], handler: handle_event)
}
func handle_profile_contact_event(_ ev: NostrEvent) { func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(state: damus, ev: ev) process_contact_event(state: damus, ev: ev)
@@ -128,11 +146,24 @@ class ProfileModel: ObservableObject, Equatable {
if self.events.insert(ev) { if self.events.insert(ev) {
self.objectWillChange.send() self.objectWillChange.send()
} }
if pinned_note_ids.contains(ev.id) && self.pinned_events.insert(ev) {
self.objectWillChange.send()
}
} else if ev.known_kind == .contacts { } else if ev.known_kind == .contacts {
handle_profile_contact_event(ev) handle_profile_contact_event(ev)
} } else if ev.known_kind == .relay_list {
else if ev.known_kind == .relay_list {
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
} else if ev.known_kind == .pinned_notes {
if let current_ev = self.pinned_notes_list {
guard ev.created_at > current_ev.created_at else {
return
}
pinned_events.incoming.removeAll()
pinned_events.events.removeAll()
}
self.pinned_notes_list = ev
subscribe_to_pinned_notes()
} }
seen_event.insert(ev.id) seen_event.insert(ev.id)
} }
@@ -150,6 +181,8 @@ class ProfileModel: ObservableObject, Equatable {
default: default:
return false return false
} }
} else if sub_id == self.pinned_subid {
return self.pubkey == ev.pubkey && pinned_note_ids.contains(ev.id)
} }
return self.pubkey == ev.pubkey return self.pubkey == ev.pubkey
@@ -160,7 +193,7 @@ class ProfileModel: ObservableObject, Equatable {
case .ws_event: case .ws_event:
return return
case .nostr_event(let resp): case .nostr_event(let resp):
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else { guard [self.sub_id, self.prof_subid, self.conversations_subid, self.pinned_subid].contains(resp.subid) else {
return return
} }
switch resp { switch resp {
@@ -181,12 +214,24 @@ class ProfileModel: ObservableObject, Equatable {
if resp.subid == self.conversations_subid { if resp.subid == self.conversations_subid {
conversation_events.insert(ev.id) conversation_events.insert(ev.id)
} }
if resp.subid == self.pinned_subid, self.pinned_events.insert(ev) {
self.objectWillChange.send()
}
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) { } else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
guard relay_filtered_correctly(ev, subid: resp.subid) else { guard relay_filtered_correctly(ev, subid: resp.subid) else {
break break
} }
conversation_events.insert(ev.id) conversation_events.insert(ev.id)
} else if resp.subid == self.pinned_subid {
guard relay_filtered_correctly(ev, subid: resp.subid) else {
break
}
if resp.subid == self.pinned_subid, self.pinned_events.insert(ev) {
self.objectWillChange.send()
}
} }
case .notice: case .notice:
break break
@@ -222,7 +267,7 @@ class ProfileModel: ObservableObject, Equatable {
} }
func getCappedRelayStrings() -> [String] { func getCappedRelayStrings() -> [String] {
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? [] return self.relay_urls?.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
} }
} }
-27
View File
@@ -1,27 +0,0 @@
//
// NIP17.swift
// damus
//
// Created by Terry Yiu on 6/6/25.
//
import Foundation
/// Functions and utilities for the NIP-04 spec
struct NIP17 {}
extension NIP17 {
/// Creates a sealed and gift wrapped kind 14 direct message event. The kind 14 direct message will not be signed because the message might leak to relays and become fully public.
static func giftWrappedDirectMessage(message: String, senderKeypair: FullKeypair, receiverPubkey: Pubkey) -> NostrEvent? {
let tags = [
["p", receiverPubkey.hex()] // TODO add receiver relay URL
]
guard let unsignedDM = NostrEvent(content: message, keypair: .just_pubkey(senderKeypair.pubkey), kind: NostrKind.dm.rawValue, tags: tags)
else {
return nil;
}
return try? NIP59GiftWrap.giftWrap(withRumor: unsignedDM, toRecipient: receiverPubkey, signedBy: senderKeypair)
}
}
-190
View File
@@ -1,190 +0,0 @@
//
// NIP59GiftWrap.swift
// damus
//
// Created by Terry Yiu on 6/6/25.
//
import Foundation
struct NIP59GiftWrap {
/// Creates a ``NostrEvent`` gift wrap of kind 1059 that takes a rumor, an unsigned ``NostrEvent``, and seals it in a signed ``NostrEvent`` seal event of kind 13, and then wraps that seal encrypted in the content of the gift wrap.
///
/// - Parameters:
/// - rumor: a ``NostrEvent`` that is not signed.
/// - recipient: the ``Pubkey`` of the receiver of the event. This pubkey will be used to encrypt the rumor. If `recipientAlias` is not provided, this pubkey will automatically be added as a tag to the ``NostrEvent`` gift wrap event.
/// - recipientAlias: optional ``Pubkey`` of the receiver's alias used to receive gift wraps without exposing the receiver's identity. It is not used to encrypt the rumor. If it is provided, this pubkey will automatically be added as a tag to the ``NostrEvent`` gift wrap event.
/// - tags: the list of tags to add to the ``NostrEvent`` gift wrap event in addition to the pubkey tag from `toRecipient`. This list should include any information needed to route the event to its intended recipient, such as [NIP-13 Proof of Work](https://github.com/nostr-protocol/nips/blob/master/13.md).
/// - createdAt: the creation timestamp of the seal. Note that this timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past. By default, if `createdAt` is not provided, a random timestamp within 2 days in the past will be chosen.
/// - keypair: The real ``FullKeypair`` to encrypt the rumor and sign the seal with. Note that a different random one-time use key is used to sign the gift wrap.
static func giftWrap(
withRumor rumor: NostrEvent,
toRecipient recipient: Pubkey,
recipientAlias: Pubkey? = nil,
tags: [Tag] = [],
createdAt: UInt32 = UInt32(max(0, Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800))),
signedBy fullKeypair: FullKeypair
) throws -> NostrEvent? {
guard let seal = try seal(withRumor: rumor, toRecipient: recipient, signedBy: fullKeypair) else {
throw SealEventError.sealFailed
}
return try giftWrap(withSeal: seal, toRecipient: recipient, recipientAlias: recipientAlias, tags: tags, createdAt: createdAt)
}
/// Creates a ``NostrEvent`` gift wrap of kind 1059 that takes a signed ``NostrEvent`` of kind 13, and then wraps that seal encrypted in the content of the gift wrap.
///
/// - Parameters:
/// - seal: a signed ``NostrEvent`` seal event of kind 13.
/// - recipient: the ``Pubkey`` of the receiver of the event. This pubkey will be used to encrypt the rumor. If `recipientAlias` is not provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``.
/// - recipientAlias: optional ``Pubkey`` of the receiver's alias used to receive gift wraps without exposing the receiver's identity. It is not used to encrypt the rumor. If it is provided, this pubkey will automatically be added as a tag to the ``GiftWrapEvent``.
/// - tags: the list of tags.
/// - createdAt: the creation timestamp of the seal. Note that this timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past. By default, if `createdAt` is not provided, a random timestamp within 2 days in the past will be chosen.
static func giftWrap(
withSeal seal: NostrEvent,
toRecipient recipient: Pubkey,
recipientAlias: Pubkey? = nil,
tags: [Tag] = [],
createdAt: UInt32 = UInt32(max(0, Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800))),
) throws -> NostrEvent? {
guard seal.known_kind == .seal else {
throw GiftWrapError.sealInvalid
}
let jsonData = try JSONEncoder().encode(seal)
guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else {
throw GiftWrapError.utf8EncodingFailed
}
let randomFullKeypair = generate_new_keypair()
let combinedTags = [["p", (recipientAlias ?? recipient).hex()]] + tags.map { $0.strings() }
let encryptedSeal = try NIP44v2Encryption.encrypt(plaintext: stringifiedJSON, privateKeyA: randomFullKeypair.privkey, publicKeyB: recipient)
return NostrEvent(content: encryptedSeal, keypair: randomFullKeypair.to_keypair(), kind: NostrKind.gift_wrap.rawValue, tags: combinedTags, createdAt: createdAt)
}
/// Unwraps the content of the gift wrap event and decrypts it into a ``NostrEvent`` seal event.
/// - Parameters:
/// - giftWrapEvent: The ``NostrEvent`` gift wrap kind 1059 to unwrap.
/// - privateKey: The ``Privkey`` to decrypt the content.
/// - Returns: The ``SealEvent``.
static func unwrappedSeal(giftWrapEvent: NostrEvent, using privateKey: Privkey) throws -> NostrEvent? {
guard giftWrapEvent.known_kind == .gift_wrap else {
throw GiftWrapError.giftWrapInvalid
}
guard let unwrappedSeal = try? NIP44v2Encryption.decrypt(payload: giftWrapEvent.content, privateKeyA: privateKey, publicKeyB: giftWrapEvent.pubkey) else {
throw GiftWrapError.decryptionFailed
}
guard let sealJSONData = unwrappedSeal.data(using: .utf8) else {
throw GiftWrapError.utf8EncodingFailed
}
guard let sealEvent = try? JSONDecoder().decode(NostrEvent.self, from: sealJSONData) else {
throw GiftWrapError.jsonDecodingFailed
}
return sealEvent
}
/// Creates a ``NostrEvent`` seal event of kind 13 that encrypts a rumor with the sender's private key and receiver's public key.
/// There is no p tag pointing to the receiver. There is no way to know who the rumor is for without the receiver's or the sender's private key.
/// The only public information in this event is who is signing it.
///
/// - Parameters:
/// - withRumor: a ``NostrEvent`` that is not signed.
/// - toRecipient: the ``PublicKey`` of the receiver of the event.
/// - createdAt: the creation timestamp of the seal. Note that this timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past. By default, if `createdAt` is not provided, a random timestamp within 2 days in the past will be chosen.
/// - keypair: The ``FullKeypair`` to sign with.
static func seal(
withRumor rumor: NostrEvent,
toRecipient recipient: Pubkey,
createdAt: UInt32 = UInt32(max(0, Date.now.timeIntervalSince1970 - TimeInterval.random(in: 0...172800))),
signedBy fullKeypair: FullKeypair
) throws -> NostrEvent? {
guard rumor.isRumor else {
throw SealEventError.sealSignedEvent
}
let jsonData = try JSONEncoder().encode(rumor)
guard let stringifiedJSON = String(data: jsonData, encoding: .utf8) else {
throw SealEventError.utf8EncodingFailed
}
let encryptedRumor = try NIP44v2Encryption.encrypt(plaintext: stringifiedJSON, privateKeyA: fullKeypair.privkey, publicKeyB: recipient)
return NostrEvent(content: encryptedRumor, keypair: fullKeypair.to_keypair(), kind: NostrKind.seal.rawValue, createdAt: createdAt)
}
/// Unseals the content of this seal event into a decrypted rumor.
/// - Parameters:
/// - giftWrapEvent: The ``NostrEvent`` gift wrap kind 1059 to unwrap into a seal, and then unseal to reveal the decrypted rumor.
/// - privateKey: The `PrivateKey` to decrypt the rumor.
/// - Returns: The decrypted ``NostrEvent`` rumor, where its `signature` is absent.
static func unsealedRumor(giftWrapEvent: NostrEvent, using privateKey: Privkey) throws -> NostrEvent? {
guard let sealEvent = try unwrappedSeal(giftWrapEvent: giftWrapEvent, using: privateKey) else {
return nil
}
return try unsealedRumor(sealEvent: sealEvent, using: privateKey)
}
static func unsealedRumor(
sealEvent: NostrEvent,
using privateKey: Privkey
) throws -> NostrEvent? {
guard let unsealedRumor = try? NIP44v2Encryption.decrypt(payload: sealEvent.content, privateKeyA: privateKey, publicKeyB: sealEvent.pubkey) else {
throw SealEventError.decryptionFailed
}
guard let data = unsealedRumor.data(using: .utf8) else {
throw SealEventError.rumorInvalid
}
guard let dict = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let content = dict["content"] as? String,
let pubkey = dict["pubkey"] as? String,
let author = Pubkey(hex: pubkey),
let kind = dict["kind"] as? UInt32,
let tags = dict["tags"] as? [[String]],
let createdAt = dict["created_at"] as? UInt32,
let id = dict["id"] as? String,
let noteId = NoteId(hex: id) else {
return nil
}
// guard let ev = NostrEvent(content: content, author: author, kind: kind, tags: tags, createdAt: createdAt, id: noteId, sig: Signature(Data())) else {
// return nil
// }
guard let ev = NostrEvent(content: content, keypair: .just_pubkey(author), kind: kind, tags: tags, createdAt: createdAt) else {
return nil
}
return ev
}
}
extension NostrEvent {
var isRumor: Bool {
return sig.data == Data(repeating: 0, count: 128)
}
}
enum GiftWrapError: Error {
case decryptionFailed
case jsonDecodingFailed
case keypairGenerationFailed
case pubkeyInvalid
case utf8EncodingFailed
case sealInvalid
case giftWrapInvalid
}
enum SealEventError: Error {
case decryptionFailed
case jsonDecodingFailed
case pubkeyInvalid
case sealSignedEvent
case utf8EncodingFailed
case sealFailed
case rumorInvalid
}
+24 -6
View File
@@ -448,17 +448,26 @@ func random_bytes(count: Int) -> Data {
return Data(bytes: bytes, count: count) return Data(bytes: bytes, count: count)
} }
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? { func make_boost_event(keypair: FullKeypair, boosted: NostrEvent, relayURL: RelayURL?) -> NostrEvent? {
var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag }) var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag })
tags.append(["e", boosted.id.hex(), "", "root"]) var eTagBuilder = ["e", boosted.id.hex()]
tags.append(["p", boosted.pubkey.hex()]) var pTagBuilder = ["p", boosted.pubkey.hex()]
let relayURLString = relayURL?.absoluteString
if let relayURLString {
pTagBuilder.append(relayURLString)
}
eTagBuilder.append(contentsOf: [relayURLString ?? "", "root", boosted.pubkey.hex()])
tags.append(eTagBuilder)
tags.append(pTagBuilder)
let content = event_to_json(ev: boosted) let content = event_to_json(ev: boosted)
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags) return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
} }
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? { func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙", relayURL: RelayURL?) -> NostrEvent? {
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2, guard tag.count >= 2,
(tag[0].matches_char("e") || tag[0].matches_char("p")) else { (tag[0].matches_char("e") || tag[0].matches_char("p")) else {
@@ -467,8 +476,17 @@ func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String =
ts.append(tag.strings()) ts.append(tag.strings())
} }
tags.append(["e", liked.id.hex()]) var eTagBuilder = ["e", liked.id.hex()]
tags.append(["p", liked.pubkey.hex()]) var pTagBuilder = ["p", liked.pubkey.hex()]
let relayURLString = relayURL?.absoluteString
if let relayURLString {
pTagBuilder.append(relayURLString)
}
eTagBuilder.append(contentsOf: [relayURLString ?? "", liked.pubkey.hex()])
tags.append(eTagBuilder)
tags.append(pTagBuilder)
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags) return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
} }
+2 -4
View File
@@ -13,14 +13,13 @@ enum NostrKind: UInt32, Codable {
case metadata = 0 case metadata = 0
case text = 1 case text = 1
case contacts = 3 case contacts = 3
case deprecated_dm = 4 case dm = 4
case delete = 5 case delete = 5
case boost = 6 case boost = 6
case like = 7 case like = 7
case seal = 13
case dm = 14
case chat = 42 case chat = 42
case mute_list = 10000 case mute_list = 10000
case pinned_notes = 10001
case relay_list = 10002 case relay_list = 10002
case interest_list = 10015 case interest_list = 10015
case list_deprecated = 30000 case list_deprecated = 30000
@@ -29,7 +28,6 @@ enum NostrKind: UInt32, Codable {
case zap = 9735 case zap = 9735
case zap_request = 9734 case zap_request = 9734
case highlight = 9802 case highlight = 9802
case gift_wrap = 1059
case nwc_request = 23194 case nwc_request = 23194
case nwc_response = 23195 case nwc_response = 23195
case http_auth = 27235 case http_auth = 27235
+6 -14
View File
@@ -19,17 +19,12 @@ struct QueuedRequest {
let skip_ephemeral: Bool let skip_ephemeral: Bool
} }
struct SeenEvent: Hashable {
let relay_id: RelayURL
let evid: NoteId
}
/// Establishes and manages connections and subscriptions to a list of relays. /// Establishes and manages connections and subscriptions to a list of relays.
class RelayPool { class RelayPool {
private(set) var relays: [Relay] = [] private(set) var relays: [Relay] = []
var handlers: [RelayHandler] = [] var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = [] var request_queue: [QueuedRequest] = []
var seen: Set<SeenEvent> = Set() var seen: [NoteId: Set<RelayURL>] = [:]
var counts: [RelayURL: UInt64] = [:] var counts: [RelayURL: UInt64] = [:]
var ndb: Ndb var ndb: Ndb
/// The keypair used to authenticate with relays /// The keypair used to authenticate with relays
@@ -357,15 +352,12 @@ class RelayPool {
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) { func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event { if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev { if case .event(_, let nev) = ev {
let k = SeenEvent(relay_id: relay_id, evid: nev.id) if seen[nev.id]?.contains(relay_id) == true {
if !seen.contains(k) { return
seen.insert(k)
if counts[relay_id] == nil {
counts[relay_id] = 1
} else {
counts[relay_id] = (counts[relay_id] ?? 0) + 1
}
} }
seen[nev.id, default: Set()].insert(relay_id)
counts[relay_id, default: 0] += 1
notify(.update_stats(note_id: nev.id))
} }
} }
} }
+4
View File
@@ -47,6 +47,10 @@ struct NEvent : Equatable, Hashable {
self.author = author self.author = author
self.kind = kind self.kind = kind
} }
init(event: NostrEvent, relays: [String]) {
self.init(noteid: event.id, relays: relays, author: event.pubkey, kind: event.kind)
}
} }
struct NProfile : Equatable, Hashable { struct NProfile : Equatable, Hashable {
+1
View File
@@ -45,4 +45,5 @@ class Constants {
// MARK: General constants // MARK: General constants
static let GIF_IMAGE_TYPE: String = "com.compuserve.gif" static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
static let MAX_SHARE_RELAYS = 4
} }
+1 -1
View File
@@ -87,7 +87,7 @@ enum LocalNotificationType: String {
switch nostr_kind { switch nostr_kind {
case .text: case .text:
return .mention return .mention
case .deprecated_dm: case .dm:
return .dm return .dm
case .like: case .like:
return .like return .like
-6
View File
@@ -165,12 +165,6 @@ class PostBox {
return return
} }
// Don't add event if it's a NIP-17 direct message kind or a NIP-59 seal event kind to avoid leaking private information.
// DMs should be sealed and gift wrapped.
if event.known_kind == .dm || event.known_kind == .seal {
return
}
let remaining = to ?? pool.our_descriptors.map { $0.url } let remaining = to ?? pool.our_descriptors.map { $0.url }
let after = delay.map { d in Date.now.addingTimeInterval(d) } let after = delay.map { d in Date.now.addingTimeInterval(d) }
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush) let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
+14 -3
View File
@@ -217,7 +217,16 @@ struct EventActionBar: View {
AnyView(self.action_bar_content) AnyView(self.action_bar_content)
} }
} }
var event_relay_url_strings: [String] {
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
}
return userProfile.getCappedRelayStrings()
}
var body: some View { var body: some View {
self.content self.content
.onAppear { .onAppear {
@@ -233,7 +242,9 @@ struct EventActionBar: View {
} }
} }
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) { .sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
ShareSheet(activityItems: [URL(string: "https://damus.io/" + event.id.bech32)!]) if let url = URL(string: "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))) {
ShareSheet(activityItems: [url])
}
} }
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) { .sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
@@ -262,7 +273,7 @@ struct EventActionBar: View {
func send_like(emoji: String) { func send_like(emoji: String) {
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else { let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
return return
} }
@@ -59,6 +59,16 @@ struct EventDetailBar: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
if bar.relays > 0 {
let relays = Array(state.nostrNetwork.pool.seen[target] ?? [])
NavigationLink(value: Route.UserRelays(relays: relays)) {
let nounString = pluralizedString(key: "relays_count", count: bar.relays)
let noun = Text(nounString).foregroundColor(.gray)
Text("\(Text(verbatim: bar.relays.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many relays a note was found on. In source English, the first variable is the number of relays, and the second variable is 'Relay' or 'Relays'.")
}
.buttonStyle(PlainButtonStyle())
}
} }
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@ struct RepostAction: View {
dismiss() dismiss()
guard let keypair = self.damus_state.keypair.to_full(), guard let keypair = self.damus_state.keypair.to_full(),
let boost = make_boost_event(keypair: keypair, boosted: self.event) else { let boost = make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
return return
} }
+11 -2
View File
@@ -26,7 +26,16 @@ struct ShareAction: View {
self.userProfile = userProfile self.userProfile = userProfile
self._show_share = show_share self._show_share = show_share
} }
var event_relay_url_strings: [String] {
let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
}
return userProfile.getCappedRelayStrings()
}
var body: some View { var body: some View {
VStack { VStack {
@@ -40,7 +49,7 @@ struct ShareAction: View {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) { ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
dismiss() dismiss()
UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(noteid: event.id, relays: userProfile.getCappedRelayStrings()))) UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))
} }
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark" let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
+1 -1
View File
@@ -37,7 +37,7 @@ struct BookmarksView: View {
} }
} else { } else {
ScrollView { ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter) InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), pinned_events: EventHolder(), damus: state, filter: noneFilter)
} }
.padding(.bottom, 10 + tabHeight + getSafeAreaBottom()) .padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
} }
+1 -1
View File
@@ -235,7 +235,7 @@ struct ChatEventView: View {
func send_like(emoji: String) { func send_like(emoji: String) {
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else { let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
return return
} }
+2 -28
View File
@@ -108,7 +108,7 @@ struct DMChatView: View, KeyboardReadable {
Button( Button(
role: .none, role: .none,
action: { action: {
send_message_nip04() send_message()
} }
) { ) {
Label("", image: "send") Label("", image: "send")
@@ -124,33 +124,7 @@ struct DMChatView: View, KeyboardReadable {
*/ */
} }
func send_message_nip17() { func send_message() {
guard let fullKeypair = damus_state.keypair.to_full() else {
return
}
let tags = [["p", pubkey.hex()]]
let post_blocks = parse_post_blocks(content: dms.draft)
let content = post_blocks
.map(\.asString)
.joined(separator: "")
guard let fullKeypair = damus_state.keypair.to_full(),
let dm = NIP17.giftWrappedDirectMessage(message: content, senderKeypair: fullKeypair, receiverPubkey: pubkey)
else {
return
}
dms.draft = ""
damus_state.nostrNetwork.postbox.send(dm)
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
end_editing()
}
func send_message_nip04() {
let tags = [["p", pubkey.hex()]] let tags = [["p", pubkey.hex()]]
let post_blocks = parse_post_blocks(content: dms.draft) let post_blocks = parse_post_blocks(content: dms.draft)
let content = post_blocks let content = post_blocks
+6 -2
View File
@@ -21,17 +21,21 @@ struct EventView: View {
let options: EventViewOptions let options: EventViewOptions
let damus: DamusState let damus: DamusState
let pubkey: Pubkey let pubkey: Pubkey
let pinned: Set<NoteId>
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, options: EventViewOptions = []) { init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, pinned: Set<NoteId> = [], options: EventViewOptions = []) {
self.event = event self.event = event
self.options = options self.options = options
self.damus = damus self.damus = damus
self.pubkey = pubkey ?? event.pubkey self.pubkey = pubkey ?? event.pubkey
self.pinned = pinned
} }
var body: some View { var body: some View {
VStack { VStack {
if event.known_kind == .boost { if pinned.contains(event.id) {
PinnedEventView(damus: damus, event: event, options: options)
} else if event.known_kind == .boost {
if let inner_ev = event.get_inner_event(cache: damus.events) { if let inner_ev = event.get_inner_event(cache: damus.events) {
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options) RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
} else { } else {
+12 -3
View File
@@ -63,7 +63,16 @@ struct MenuItems: View {
self.target_pubkey = target_pubkey self.target_pubkey = target_pubkey
self.profileModel = profileModel self.profileModel = profileModel
} }
var event_relay_url_strings: [String] {
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
}
return profileModel.getCappedRelayStrings()
}
var body: some View { var body: some View {
Group { Group {
Button { Button {
@@ -79,7 +88,7 @@ struct MenuItems: View {
} }
Button { Button {
UIPasteboard.general.string = event.id.bech32 UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))
} label: { } label: {
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")
} }
@@ -108,7 +117,7 @@ struct MenuItems: View {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), image: "globe") Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), image: "globe")
} }
// 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 != .deprecated_dm && event.known_kind != .dm && event.known_kind != .seal { if event.known_kind != .dm {
MuteDurationMenu { duration in MuteDurationMenu { duration in
if let full_keypair = self.damus_state.keypair.to_full(), 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(), 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)) {
@@ -98,7 +98,7 @@ struct FollowPackView: View {
} }
if tab_selection == FollowPackTabSelection.posts { if tab_selection == FollowPackTabSelection.posts {
InnerTimelineView(events: model.events, damus: state, filter: content_filter(event.publicKeys)) InnerTimelineView(events: model.events, pinned_events: EventHolder(), damus: state, filter: content_filter(event.publicKeys))
} }
} }
.onAppear() { .onAppear() {
@@ -0,0 +1,28 @@
//
// PinnedEventView.swift
// damus
//
// Created by Terry Yiu on 7/21/25.
//
import SwiftUI
struct PinnedEventView: View {
let damus: DamusState
let event: NostrEvent
let options: EventViewOptions
var body: some View {
VStack(alignment: .leading) {
PinnedHeaderView(damus: damus, pubkey: event.pubkey)
.padding(.horizontal)
.buttonStyle(PlainButtonStyle())
TextEvent(damus: damus, event: event, pubkey: event.pubkey, options: options)
}
}
}
#Preview {
PinnedEventView(damus: test_damus_state, event: test_note, options: [])
}
@@ -0,0 +1,33 @@
//
// PinnedHeaderView.swift
// damus
//
// Created by Terry Yiu on 7/21/25.
//
import SwiftUI
struct PinnedHeaderView: View {
let damus: DamusState
let pubkey: Pubkey
init(damus: DamusState, pubkey: Pubkey) {
self.damus = damus
self.pubkey = pubkey
}
var body: some View {
HStack(alignment: .center) {
Image("pin")
.foregroundColor(Color.gray)
Text("Pinned", comment: "FIXME")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
#Preview {
PinnedHeaderView(damus: test_damus_state, pubkey: test_pubkey)
}
+2 -2
View File
@@ -64,7 +64,7 @@ class LoadableNostrEventViewModel: ObservableObject {
switch known_kind { switch known_kind {
case .text, .highlight: case .text, .highlight:
return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state))) return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state)))
case .deprecated_dm: // FIXME(tyiu) case .dm:
let dm_model = damus_state.dms.lookup_or_create(ev.pubkey) let dm_model = damus_state.dms.lookup_or_create(ev.pubkey)
return .loaded(route: Route.DMChat(dms: dm_model)) return .loaded(route: Route.DMChat(dms: dm_model))
case .like: case .like:
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
case .zap, .zap_request: case .zap, .zap_request:
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found } guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
return .loaded(route: Route.Zaps(target: zap.target)) return .loaded(route: Route.Zaps(target: zap.target))
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .dm, .seal, .gift_wrap: case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .pinned_notes:
return .unknown_or_unsupported_kind return .unknown_or_unsupported_kind
} }
case .naddr(let naddr): case .naddr(let naddr):
+13 -9
View File
@@ -798,18 +798,18 @@ private func isAlphanumeric(_ char: Character) -> Bool {
return char.isLetter || char.isNumber return char.isLetter || char.isNumber
} }
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] { func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: RelayURL?) -> [[String]] {
guard let nip10 = replying_to.thread_reply() else { guard let nip10 = replying_to.thread_reply() else {
// we're replying to a post that isn't in a thread, // we're replying to a post that isn't in a thread,
// just add a single reply-to-root tag // just add a single reply-to-root tag
return [["e", replying_to.id.hex(), "", "root"]] return [["e", replying_to.id.hex(), relayURL?.absoluteString ?? "", "root"]]
} }
// otherwise use the root tag from the parent's nip10 reply and include the note // otherwise use the root tag from the parent's nip10 reply and include the note
// that we are replying to's note id. // that we are replying to's note id.
let tags = [ let tags = [
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"], ["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
["e", replying_to.id.hex(), "", "reply"] ["e", replying_to.id.hex(), relayURL?.absoluteString ?? "", "reply"]
] ]
return tags return tags
@@ -902,15 +902,19 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
switch action { switch action {
case .replying_to(let replying_to): case .replying_to(let replying_to):
// start off with the reply tags // start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair) tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first)
case .quoting(let ev): case .quoting(let ev):
content.append("\n\nnostr:" + bech32_note_id(ev.id)) let relay_urls = state.nostrNetwork.relaysForEvent(event: ev)
let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0.absoluteString })))
content.append("\n\nnostr:\(nevent)")
tags.append(["q", ev.id.hex()]); if let first_relay = relay_urls.first?.absoluteString {
tags.append(["q", ev.id.hex(), first_relay, ev.pubkey.hex()]);
if let quoted_ev = state.events.lookup(ev.id) { tags.append(["p", ev.pubkey.hex(), first_relay])
tags.append(["p", quoted_ev.pubkey.hex()]) } else {
tags.append(["q", ev.id.hex(), "", ev.pubkey.hex()]);
tags.append(["p", ev.pubkey.hex()])
} }
case .posting, .highlighting, .sharing: case .posting, .highlighting, .sharing:
break break
+3 -3
View File
@@ -464,13 +464,13 @@ struct ProfileView: View {
.background(colorScheme == .dark ? Color.black : Color.white) .background(colorScheme == .dark ? Color.black : Color.white)
if filter_state == FilterState.posts { if filter_state == FilterState.posts {
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts)) InnerTimelineView(events: profile.events, pinned_events: profile.pinned_events, damus: damus_state, filter: content_filter(FilterState.posts))
} }
if filter_state == FilterState.posts_and_replies { if filter_state == FilterState.posts_and_replies {
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies)) InnerTimelineView(events: profile.events, pinned_events: profile.pinned_events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
} }
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty { if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations)) InnerTimelineView(events: profile.events, pinned_events: EventHolder(), damus: damus_state, filter: content_filter(FilterState.conversations))
} }
} }
.padding(.horizontal, Theme.safeAreaInsets?.left) .padding(.horizontal, Theme.safeAreaInsets?.left)
+6 -4
View File
@@ -10,11 +10,13 @@ import SwiftUI
struct InnerTimelineView: View { struct InnerTimelineView: View {
@ObservedObject var events: EventHolder @ObservedObject var events: EventHolder
@ObservedObject var pinned_events: EventHolder
let state: DamusState let state: DamusState
let filter: (NostrEvent) -> Bool let filter: (NostrEvent) -> Bool
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) { init(events: EventHolder, pinned_events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
self.events = events self.events = events
self.pinned_events = pinned_events
self.state = damus self.state = damus
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
} }
@@ -29,7 +31,7 @@ struct InnerTimelineView: View {
var body: some View { var body: some View {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
let events = self.events.events let events = self.pinned_events.events + self.events.events
if events.isEmpty { if events.isEmpty {
EmptyTimelineView() EmptyTimelineView()
} else { } else {
@@ -38,7 +40,7 @@ struct InnerTimelineView: View {
ForEach(indexed, id: \.0.id) { tup in ForEach(indexed, id: \.0.id) { tup in
let ev = tup.0 let ev = tup.0
let ind = tup.1 let ind = tup.1
EventView(damus: state, event: ev, options: event_options) EventView(damus: state, event: ev, pinned: Set(self.pinned_events.events.map { $0.id }), options: event_options)
.onTapGesture { .onTapGesture {
let event = ev.get_inner_event(cache: state.events) ?? ev let event = ev.get_inner_event(cache: state.events) ?? ev
let thread = ThreadModel(event: event, damus_state: state) let thread = ThreadModel(event: event, damus_state: state)
@@ -69,7 +71,7 @@ struct InnerTimelineView: View {
struct InnerTimelineView_Previews: PreviewProvider { struct InnerTimelineView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
InnerTimelineView(events: test_event_holder, damus: test_damus_state, filter: { _ in true }) InnerTimelineView(events: test_event_holder, pinned_events: EventHolder(), damus: test_damus_state, filter: { _ in true })
.frame(width: 300, height: 500) .frame(width: 300, height: 500)
.border(Color.red) .border(Color.red)
} }
+1 -1
View File
@@ -61,7 +61,7 @@ struct TimelineView<Content: View>: View {
.id("startblock") .id("startblock")
.frame(height: 0) .frame(height: 0)
InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules) InnerTimelineView(events: events, pinned_events: EventHolder(), damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.redacted(reason: loading ? .placeholder : []) .redacted(reason: loading ? .placeholder : [])
.shimmer(loading) .shimmer(loading)
.disabled(loading) .disabled(loading)
Binary file not shown.
+32
View File
@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Nutzer</string>
<key>other</key>
<string>Nutzer</string>
</dict>
</dict>
<key>followed_by_three_and_others</key> <key>followed_by_three_and_others</key>
<dict> <dict>
<key>NSStringLocalizedFormatKey</key> <key>NSStringLocalizedFormatKey</key>
@@ -82,6 +98,22 @@
<string>Imporieren</string> <string>Imporieren</string>
</dict> </dict>
</dict> </dict>
<key>notes_from_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Notizen von %2$@, %3$@, %4$@ &amp; %1$d weiterem aus deinem vertrauenswürdigen Netzwerk</string>
<key>other</key>
<string>Notizen von %2$@, %3$@, %4$@ &amp; %1$d weiteren aus deinem vertrauenswürdigen Netzwerk</string>
</dict>
</dict>
<key>people_reposted_count</key> <key>people_reposted_count</key>
<dict> <dict>
<key>NSStringLocalizedFormatKey</key> <key>NSStringLocalizedFormatKey</key>
+17 -1
View File
@@ -167,7 +167,23 @@ class Bech32ObjectTests: XCTestCase {
XCTAssertEqual(expectedEncoding, actualEncoding) XCTAssertEqual(expectedEncoding, actualEncoding)
} }
func testTLVEncoding_NeventFromNostrEvent_ValidContent() throws {
let relays = ["wss://relay.damus.io", "wss://relay.nostr.band"]
let nevent = NEvent(event: test_note, relays: relays)
XCTAssertEqual(nevent.noteid, test_note.id)
XCTAssertEqual(nevent.relays, relays)
XCTAssertEqual(nevent.author, test_note.pubkey)
XCTAssertEqual(nevent.kind, test_note.kind)
let expectedEncoding = "nevent1qqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3vamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmnyqgsgydql3q4ka27d9wnlrmus4tvkrnc8ftc4h8h5fgyln54gl0a7dgsrqsqqqqqpppe7n6"
let actualEncoding = Bech32Object.encode(.nevent(NEvent(event: test_note, relays: relays)))
XCTAssertEqual(expectedEncoding, actualEncoding)
}
func testTLVEncoding_NProfileExample_ValidContent() throws { func testTLVEncoding_NProfileExample_ValidContent() throws {
guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else { guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else {
XCTFail() XCTFail()
+7 -7
View File
@@ -25,7 +25,7 @@ class LikeTests: XCTestCase {
keypair: test_keypair, keypair: test_keypair,
tags: [cindy.tag, bob.tag])! tags: [cindy.tag, bob.tag])!
let id = liked.id let id = liked.id
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)! let like_ev = make_like_event(keypair: test_keypair_full, liked: liked, relayURL: nil)!
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey)) XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy)) XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy))
@@ -36,12 +36,12 @@ class LikeTests: XCTestCase {
func testToReactionEmoji() { func testToReactionEmoji() {
let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])! let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])!
let emptyReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "")! let emptyReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "", relayURL: nil)!
let plusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "+")! let plusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "+", relayURL: nil)!
let minusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "-")! let minusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "-", relayURL: nil)!
let heartReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "❤️")! let heartReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "❤️", relayURL: nil)!
let thumbsUpReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "👍")! let thumbsUpReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "👍", relayURL: nil)!
let shakaReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "🤙")! let shakaReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "🤙", relayURL: nil)!
XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️") XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️")
XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️") XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️")
-13
View File
@@ -40,17 +40,4 @@ final class NostrEventTests: XCTestCase {
let urlInContent2 = "https://cdn.nostr.build/i/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg" let urlInContent2 = "https://cdn.nostr.build/i/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg"
XCTAssert(testEvent2.content.contains(urlInContent2), "Issue parsing event. Expected to see '\(urlInContent2)' inside \(testEvent2.content)") XCTAssert(testEvent2.content.contains(urlInContent2), "Issue parsing event. Expected to see '\(urlInContent2)' inside \(testEvent2.content)")
} }
func testNostrEventWithoutPrivateKey() throws {
let event = NostrEvent(
content: "Test",
keypair: .just_pubkey(test_pubkey),
kind: NostrKind.dm.rawValue,
tags: []
)
let nonNilEvent = try XCTUnwrap(event)
let json = try JSONEncoder().encode(nonNilEvent)
print(json)
}
} }
+1 -1
View File
@@ -174,7 +174,7 @@ final class PostViewTests: XCTestCase {
func testQuoteRepost() { func testQuoteRepost() {
let post = build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: []) let post = build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: [])
XCTAssertEqual(post.tags, [["q", test_note.id.hex()]]) XCTAssertEqual(post.tags, [["q", test_note.id.hex(), "", jack_keypair.pubkey.hex()], ["p", jack_keypair.pubkey.hex()]])
} }
func testBuildPostRecognizesStringsAsNpubs() throws { func testBuildPostRecognizesStringsAsNpubs() throws {
+5 -5
View File
@@ -174,9 +174,9 @@ class NdbNote: Codable, Equatable, Hashable {
let tags = try container.decode([[String]].self, forKey: .tags) let tags = try container.decode([[String]].self, forKey: .tags)
let createdAt = try container.decode(UInt32.self, forKey: .created_at) let createdAt = try container.decode(UInt32.self, forKey: .created_at)
let noteId = try container.decode(NoteId.self, forKey: .id) let noteId = try container.decode(NoteId.self, forKey: .id)
let signature = try? container.decode(Signature.self, forKey: .sig) let signature = try container.decode(Signature.self, forKey: .sig)
guard let note = NdbNote.init(content: content, author: pubkey, kind: kind, tags: tags, createdAt: createdAt, id: noteId, sig: signature ?? Signature(Data(repeating: 0, count: 128))) else { guard let note = NdbNote.init(content: content, author: pubkey, kind: kind, tags: tags, createdAt: createdAt, id: noteId, sig: signature) else {
throw DecodingError.initializationFailed throw DecodingError.initializationFailed
} }
@@ -456,7 +456,7 @@ extension NdbNote {
} }
func get_content(_ keypair: Keypair) -> String { func get_content(_ keypair: Keypair) -> String {
if known_kind == .deprecated_dm { if known_kind == .dm {
return decrypted(keypair: keypair) ?? "*failed to decrypt content*" return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
} }
else if known_kind == .highlight { else if known_kind == .highlight {
@@ -467,7 +467,7 @@ extension NdbNote {
} }
func maybe_get_content(_ keypair: Keypair) -> String? { func maybe_get_content(_ keypair: Keypair) -> String? {
if known_kind == .deprecated_dm { if known_kind == .dm {
return decrypted(keypair: keypair) return decrypted(keypair: keypair)
} }
-33
View File
@@ -3628,39 +3628,6 @@ int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note,
return total_size; return total_size;
} }
int ndb_builder_finalize_just_pubkey(struct ndb_builder *builder, struct ndb_note **note,
unsigned char pubkey[32])
{
int strings_len = builder->strings.p - builder->strings.start;
unsigned char *note_end = builder->note_cur.p + strings_len;
int total_size = note_end - builder->note_cur.start;
// move the strings buffer next to the end of our ndb_note
memmove(builder->note_cur.p, builder->strings.start, strings_len);
// set the strings location
builder->note->strings = builder->note_cur.p - builder->note_cur.start;
// record the total size
//builder->note->size = total_size;
*note = builder->note;
// use the remaining memory for building our id buffer
unsigned char *end = builder->mem.end;
unsigned char *start = (unsigned char*)(*note) + total_size;
ndb_builder_set_pubkey(builder, pubkey);
if (!ndb_calculate_id(builder->note, start, end - start))
return 0;
// make sure we're aligned as a whole
total_size = (total_size + 7) & ~7;
assert((total_size % 8) == 0);
return total_size;
}
struct ndb_note * ndb_builder_note(struct ndb_builder *builder) struct ndb_note * ndb_builder_note(struct ndb_builder *builder)
{ {
return builder->note; return builder->note;
-1
View File
@@ -366,7 +366,6 @@ int ndb_ws_event_from_json(const char *json, int len, struct ndb_tce *tce, unsig
int ndb_note_from_json(const char *json, int len, struct ndb_note **, unsigned char *buf, int buflen); int ndb_note_from_json(const char *json, int len, struct ndb_note **, unsigned char *buf, int buflen);
int ndb_builder_init(struct ndb_builder *builder, unsigned char *buf, int bufsize); int ndb_builder_init(struct ndb_builder *builder, unsigned char *buf, int bufsize);
int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, struct ndb_keypair *privkey); int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, struct ndb_keypair *privkey);
int ndb_builder_finalize_just_pubkey(struct ndb_builder *builder, struct ndb_note **note, unsigned char pubkey[32]);
int ndb_builder_set_content(struct ndb_builder *builder, const char *content, int len); int ndb_builder_set_content(struct ndb_builder *builder, const char *content, int len);
void ndb_builder_set_created_at(struct ndb_builder *builder, uint64_t created_at); void ndb_builder_set_created_at(struct ndb_builder *builder, uint64_t created_at);
void ndb_builder_set_sig(struct ndb_builder *builder, unsigned char *sig); void ndb_builder_set_sig(struct ndb_builder *builder, unsigned char *sig);