commit 14bf187a6e8a66628f6189fff92d73daaf426cdb parent e9c1671d06f74c38bc62348f84db353b48ec2a38 Author: William Casarin <jb55@jb55.com> Date: Sun, 1 Jun 2025 00:35:35 +0200 Merge remote-tracking branches 'github/pr/30{62,57,55,51,50}' Merge a bunch of changes from terry, translations, and me Terry Yiu (4): Add NIP-05 favicon to profile names and NIP-05 web of trust feed Fix quotes view header alignment Export strings for translation Rename Bitcoin Beach wallet to Blink Transifex (11): Translate Localizable.strings in th Translate Localizable.strings in th Translate Localizable.strings in nl Translate Localizable.strings in de Translate Localizable.stringsdict in de Translate Localizable.stringsdict in de Translate Localizable.strings in th Translate Localizable.strings in th Translate Localizable.strings in th Translate Localizable.strings in th Translate Localizable.strings in th William Casarin (2): perf: don't use regex in trim_{prefix,suffix} Diffstat:
34 files changed, 1141 insertions(+), 91 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -14,6 +14,12 @@ 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; }; 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; }; + 3A2BAC5A2DD7E4C400EBB4CC /* 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 */; }; + 3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; }; + 3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; }; + 3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; }; 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; }; 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; }; 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; }; @@ -22,6 +28,10 @@ 3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; }; 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; + 3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; }; + 3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; }; + 3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; }; + 3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */; }; 3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; }; @@ -33,6 +43,15 @@ 3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; }; 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; }; 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; }; + 3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF94372DA9A52F00971A4E /* FaviconFinder */; }; + 3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943D2DA9B10800971A4E /* FaviconFinder */; }; + 3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943F2DA9B11200971A4E /* FaviconFinder */; }; + 3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; }; + 3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; }; + 3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; }; + 3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; }; + 3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; }; + 3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; }; 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; 4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; }; @@ -47,6 +66,7 @@ 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; 4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C03972A61E27B0098B3B8 /* primal.wasm */; }; 4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */; }; + 4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */; }; 4C1253502A76C5B20004F4B8 /* UnfollowedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C12534F2A76C5B20004F4B8 /* UnfollowedNotify.swift */; }; 4C1253522A76C6130004F4B8 /* ComposeNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253512A76C6130004F4B8 /* ComposeNotify.swift */; }; 4C1253542A76C7D60004F4B8 /* LogoutNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253532A76C7D60004F4B8 /* LogoutNotify.swift */; }; @@ -1808,6 +1828,8 @@ 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>"; }; 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>"; }; + 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; }; 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; }; 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; }; 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; }; @@ -1853,6 +1875,8 @@ 3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; + 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = "<group>"; }; + 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderViewTests.swift; sourceTree = "<group>"; }; 3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; @@ -1891,6 +1915,8 @@ 3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; }; + 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineView.swift; sourceTree = "<group>"; }; + 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainEventsModel.swift; sourceTree = "<group>"; }; 3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; }; @@ -1925,6 +1951,7 @@ 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; 4C0C03972A61E27B0098B3B8 /* primal.wasm */ = {isa = PBXFileReference; lastKnownFileType = file; name = primal.wasm; path = nostrscript/primal.wasm; sourceTree = SOURCE_ROOT; }; 4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */ = {isa = PBXFileReference; lastKnownFileType = file; name = bool_setting.wasm; path = nostrscript/bool_setting.wasm; sourceTree = SOURCE_ROOT; }; + 4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmarking.swift; sourceTree = "<group>"; }; 4C12534F2A76C5B20004F4B8 /* UnfollowedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfollowedNotify.swift; sourceTree = "<group>"; }; 4C1253512A76C6130004F4B8 /* ComposeNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeNotify.swift; sourceTree = "<group>"; }; 4C1253532A76C7D60004F4B8 /* LogoutNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutNotify.swift; sourceTree = "<group>"; }; @@ -2617,6 +2644,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */, 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */, 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */, @@ -2648,6 +2676,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */, 82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */, D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */, 82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */, @@ -2664,6 +2693,7 @@ buildActionMask = 2147483647; files = ( D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */, + 3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */, D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */, D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */, D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */, @@ -2841,6 +2871,7 @@ 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */, D773BC5E2C6D538500349F0A /* CommentItem.swift */, D767066E2C8BB3CE00F09726 /* URLHandler.swift */, + 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -3255,6 +3286,9 @@ D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */, D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */, D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */, + 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */, + 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */, + 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */, ); path = Views; sourceTree = "<group>"; @@ -3377,6 +3411,7 @@ D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */, D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */, D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */, + 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */, ); path = Util; sourceTree = "<group>"; @@ -3772,6 +3807,8 @@ 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */, D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */, 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */, + 4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */, + 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */, ); path = damusTests; sourceTree = "<group>"; @@ -4173,6 +4210,7 @@ D70D90972CDED61800CD0534 /* CodeScanner */, D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */, D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */, + 3ACF94372DA9A52F00971A4E /* FaviconFinder */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -4240,6 +4278,7 @@ D7F360282CEBBE34009D34DA /* CodeScanner */, D7C48C0C2D12E34900A3BACF /* SwiftyCrop */, D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */, + 3ACF943F2DA9B11200971A4E /* FaviconFinder */, ); productName = "share extension"; productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */; @@ -4269,6 +4308,7 @@ D70D909B2CDED7B200CD0534 /* CodeScanner */, D7C48C0E2D12E35600A3BACF /* SwiftyCrop */, D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */, + 3ACF943D2DA9B10800971A4E /* FaviconFinder */, ); productName = "highlighter action extension"; productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */; @@ -4381,6 +4421,7 @@ D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */, D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */, D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */, + 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -4523,6 +4564,7 @@ 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */, 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */, + 3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */, D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */, @@ -4561,6 +4603,7 @@ 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */, D798D2282B085CDA00234419 /* NdbNote+.swift in Sources */, + 3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */, 4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */, @@ -4757,6 +4800,7 @@ 4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */, D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, + 3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */, D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */, 4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */, D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */, @@ -4916,6 +4960,7 @@ 4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */, 4C32B9592A9AD44700DC3548 /* Table.swift in Sources */, 4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */, + 3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, @@ -4963,6 +5008,7 @@ 4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */, 4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */, 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, + 3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */, 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */, 5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, @@ -5002,6 +5048,7 @@ 4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */, 4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */, D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */, + 4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */, 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */, 3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */, @@ -5045,6 +5092,7 @@ 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */, 4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */, E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */, + 3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5130,6 +5178,7 @@ 82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */, 82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */, D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */, + 3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */, 82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */, 82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */, 82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */, @@ -5148,9 +5197,11 @@ 82D6FAF62CD99F7900C925F4 /* ZappingNotify.swift in Sources */, 82D6FAF72CD99F7900C925F4 /* MuteNotify.swift in Sources */, 82D6FAF82CD99F7900C925F4 /* RelaysChangedNotify.swift in Sources */, + 3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */, 82D6FAF92CD99F7900C925F4 /* MuteThreadNotify.swift in Sources */, 82D6FAFA2CD99F7900C925F4 /* UnmuteThreadNotify.swift in Sources */, 82D6FAFB2CD99F7900C925F4 /* ReconnectRelaysNotify.swift in Sources */, + 3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */, 82D6FAFC2CD99F7900C925F4 /* PurpleAccountUpdateNotify.swift in Sources */, 82D6FAFD2CD99F7900C925F4 /* IdType.swift in Sources */, 82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */, @@ -5403,6 +5454,7 @@ 82D6FBED2CD99F7900C925F4 /* MediaView.swift in Sources */, 82D6FBEE2CD99F7900C925F4 /* PurpleViewPrimitives.swift in Sources */, 82D6FBEF2CD99F7900C925F4 /* MarketingContentView.swift in Sources */, + 3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */, 82D6FBF02CD99F7900C925F4 /* LogoView.swift in Sources */, 82D6FBF12CD99F7900C925F4 /* IAPProductStateView.swift in Sources */, 82D6FBF22CD99F7900C925F4 /* PurpleBackdrop.swift in Sources */, @@ -5421,6 +5473,7 @@ 82D6FBFF2CD99F7900C925F4 /* NotificationItemView.swift in Sources */, 82D6FC002CD99F7900C925F4 /* ProfilePicturesView.swift in Sources */, 82D6FC012CD99F7900C925F4 /* DamusAppNotificationView.swift in Sources */, + 3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */, 82D6FC022CD99F7900C925F4 /* InnerTimelineView.swift in Sources */, 82D6FC032CD99F7900C925F4 /* PostingTimelineView.swift in Sources */, 82D6FC042CD99F7900C925F4 /* ZapsView.swift in Sources */, @@ -5636,11 +5689,13 @@ D73E5E682C6A97F4007EB227 /* VectorMath.swift in Sources */, D73E5E692C6A97F4007EB227 /* RelayBootstrap.swift in Sources */, D73E5E6A2C6A97F4007EB227 /* RelayModel.swift in Sources */, + 3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */, D73E5E6B2C6A97F4007EB227 /* AnyCodable.swift in Sources */, D73E5E6C2C6A97F4007EB227 /* AnyDecodable.swift in Sources */, D73E5E6D2C6A97F4007EB227 /* AnyEncodable.swift in Sources */, D73E5F782C6A9A5C007EB227 /* NdbNote+.swift in Sources */, D73E5E6E2C6A97F4007EB227 /* NIPURLBuilder.swift in Sources */, + 3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */, D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */, D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */, D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */, @@ -5727,6 +5782,7 @@ D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */, D73E5EBA2C6A97F4007EB227 /* NostrFilter.swift in Sources */, D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */, + 3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */, D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */, D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */, 5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */, @@ -5825,6 +5881,7 @@ D73E5F192C6A97F4007EB227 /* RelayToggle.swift in Sources */, D73E5F1A2C6A97F4007EB227 /* RelayStatusView.swift in Sources */, D73E5F1B2C6A97F4007EB227 /* RelayType.swift in Sources */, + 3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */, D73E5F1C2C6A97F4007EB227 /* SignalView.swift in Sources */, D73E5F1D2C6A97F4007EB227 /* RelayPicView.swift in Sources */, D73E5F1E2C6A97F4007EB227 /* UserSearch.swift in Sources */, @@ -5957,6 +6014,7 @@ D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */, D703D7A92C670E5A00A400EA /* refmap.c in Sources */, D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */, + 3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */, D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */, D703D79D2C670E0700A400EA /* node_id.c in Sources */, D703D79B2C670E0000A400EA /* bech32_util.c in Sources */, @@ -6939,6 +6997,14 @@ minimumVersion = 0.2.0; }; }; + 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/will-lumley/FaviconFinder.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.1.4; + }; + }; 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; @@ -7019,6 +7085,21 @@ package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */; productName = EmojiPicker; }; + 3ACF94372DA9A52F00971A4E /* FaviconFinder */ = { + isa = XCSwiftPackageProductDependency; + package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */; + productName = FaviconFinder; + }; + 3ACF943D2DA9B10800971A4E /* FaviconFinder */ = { + isa = XCSwiftPackageProductDependency; + package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */; + productName = FaviconFinder; + }; + 3ACF943F2DA9B11200971A4E /* FaviconFinder */ = { + isa = XCSwiftPackageProductDependency; + package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */; + productName = FaviconFinder; + }; 4C06670328FC7EC500038D2A /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac", + "originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc", "pins" : [ { "identity" : "codescanner", @@ -36,6 +36,15 @@ } }, { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c", + "version" : "5.1.4" + } + }, + { "identity" : "gsplayer", "kind" : "remoteSourceControl", "location" : "https://github.com/wxxsw/GSPlayer", @@ -106,6 +115,15 @@ } }, { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "bba848db50462894e7fc0891d018dfecad4ef11e", + "version" : "2.8.7" + } + }, + { "identity" : "swiftycrop", "kind" : "remoteSourceControl", "location" : "https://github.com/benedom/SwiftyCrop", diff --git a/damus/Assets.xcassets/Logos/bbw.imageset/Contents.json b/damus/Assets.xcassets/Logos/bbw.imageset/Contents.json @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "bbw.jpg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/damus/Assets.xcassets/Logos/bbw.imageset/bbw.jpg b/damus/Assets.xcassets/Logos/bbw.imageset/bbw.jpg Binary files differ. diff --git a/damus/Assets.xcassets/Logos/blink.imageset/Contents.json b/damus/Assets.xcassets/Logos/blink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "blink.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/Logos/blink.imageset/blink.png b/damus/Assets.xcassets/Logos/blink.imageset/blink.png Binary files differ. diff --git a/damus/Components/NIP05Badge.swift b/damus/Components/NIP05Badge.swift @@ -5,27 +5,27 @@ // Created by William Casarin on 2023-01-11. // +import FaviconFinder +import Kingfisher import SwiftUI struct NIP05Badge: View { let nip05: NIP05 let pubkey: Pubkey - let contacts: Contacts + let damus_state: DamusState let show_domain: Bool - let profiles: Profiles + let nip05_domain_favicon: FaviconURL? - @Environment(\.openURL) var openURL - - init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) { + init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) { self.nip05 = nip05 self.pubkey = pubkey - self.contacts = contacts + self.damus_state = damus_state self.show_domain = show_domain - self.profiles = profiles + self.nip05_domain_favicon = nip05_domain_favicon } var nip05_color: Bool { - return use_nip05_color(pubkey: pubkey, contacts: contacts) + return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts) } var Seal: some View { @@ -44,8 +44,23 @@ struct NIP05Badge: View { } } + var domainBadge: some View { + Group { + if let nip05_domain_favicon { + KFImage(nip05_domain_favicon.source) + .imageContext(.favicon, disable_animation: true) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .clipped() + } else { + EmptyView() + } + } + } + var username_matches_nip05: Bool { - guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value + guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value else { return false } @@ -65,14 +80,18 @@ struct NIP05Badge: View { HStack(spacing: 2) { Seal - if show_domain { - Text(nip05_string) - .nip05_colorized(gradient: nip05_color) - .onTapGesture { - if let nip5url = nip05.siteUrl { - openURL(nip5url) - } - } + Group { + if show_domain { + Text(nip05_string) + .nip05_colorized(gradient: nip05_color) + } + + if nip05_domain_favicon != nil { + domainBadge + } + } + .onTapGesture { + damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon)) } } @@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider { static var previews: some View { let test_state = test_damus_state VStack { - NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles) - - NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles) - - NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles) + NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil) - NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles) + NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil) } } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -686,7 +686,8 @@ struct ContentView: View { video: DamusVideoCoordinator(), ndb: ndb, quote_reposts: .init(our_pubkey: pubkey), - emoji_provider: DefaultEmojiProvider(showAllVariations: true) + emoji_provider: DefaultEmojiProvider(showAllVariations: true), + favicon_cache: FaviconCache() ) home.damus_state = self.damus_state! diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift @@ -38,6 +38,10 @@ class Contacts { return friends } + func get_friend_of_friends_list() -> Set<Pubkey> { + return friend_of_friends + } + func get_followed_hashtags() -> Set<String> { guard let ev = self.event else { return Set() } return Set(ev.referenced_hashtags.map({ $0.hashtag })) diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -36,9 +36,10 @@ class DamusState: HeadlessDamusState { var purple: DamusPurple var push_notification_client: PushNotificationClient let emoji_provider: EmojiProvider + let favicon_cache: FaviconCache private(set) var nostrNetwork: NostrNetworkManager - init(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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) { + init(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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) { self.keypair = keypair self.likes = likes self.boosts = boosts @@ -68,7 +69,8 @@ class DamusState: HeadlessDamusState { self.quote_reposts = quote_reposts self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings) self.emoji_provider = emoji_provider - + self.favicon_cache = FaviconCache() + let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters) self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate) } @@ -126,7 +128,8 @@ class DamusState: HeadlessDamusState { video: DamusVideoCoordinator(), ndb: ndb, quote_reposts: .init(our_pubkey: pubkey), - emoji_provider: DefaultEmojiProvider(showAllVariations: true) + emoji_provider: DefaultEmojiProvider(showAllVariations: true), + favicon_cache: FaviconCache() ) } @@ -194,7 +197,8 @@ class DamusState: HeadlessDamusState { video: DamusVideoCoordinator(), ndb: .empty, quote_reposts: .init(our_pubkey: empty_pub), - emoji_provider: DefaultEmojiProvider(showAllVariations: true) + emoji_provider: DefaultEmojiProvider(showAllVariations: true), + favicon_cache: FaviconCache() ) } } diff --git a/damus/Models/NIP05DomainEventsModel.swift b/damus/Models/NIP05DomainEventsModel.swift @@ -0,0 +1,97 @@ +// +// NIP05DomainEventsModel.swift +// damus +// +// Created by Terry Yiu on 4/11/25. +// + +import FaviconFinder +import Foundation + +class NIP05DomainEventsModel: ObservableObject { + let state: DamusState + var events: EventHolder + @Published var loading: Bool = false + + let domain: String + var filter: NostrFilter + let sub_id = UUID().description + let profiles_subid = UUID().description + let limit: UInt32 = 500 + + init(state: DamusState, domain: String) { + self.state = state + self.domain = domain + self.events = EventHolder(on_queue: { ev in + preload_events(state: state, events: [ev]) + }) + self.filter = NostrFilter() + } + + @MainActor func subscribe() { + filter.limit = self.limit + filter.kinds = [.text, .longform, .highlight] + + var authors = Set<Pubkey>() + for pubkey in state.contacts.get_friend_of_friends_list() { + let profile_txn = state.profiles.lookup(id: pubkey) + + guard let profile = profile_txn?.unsafeUnownedValue, + let nip05_str = profile.nip05, + let nip05 = NIP05.parse(nip05_str), + nip05.host.caseInsensitiveCompare(domain) == .orderedSame else { + continue + } + + authors.insert(pubkey) + } + if authors.isEmpty { + return + } + filter.authors = Array(authors) + + print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)") + state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event) + loading = true + state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id))) + } + + func unsubscribe() { + state.nostrNetwork.pool.unsubscribe(sub_id: sub_id) + loading = false + print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)") + } + + func add_event(_ ev: NostrEvent) { + if !event_matches_filter(ev, filter: filter) { + return + } + + guard should_show_event(state: state, ev: ev) else { + return + } + + if self.events.insert(ev) { + objectWillChange.send() + } + } + + func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { + let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in + if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event { + self.add_event(ev) + } + } + + guard done else { + return + } + + self.loading = false + + if sub_id == self.sub_id { + guard let txn = NdbTxn(ndb: state.ndb) else { return } + load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) + } + } +} diff --git a/damus/Models/NoteContent.swift b/damus/Models/NoteContent.swift @@ -257,12 +257,20 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText // trim suffix whitespace and newlines func trim_suffix(_ str: String) -> String { - return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) + var result = str + while result.last?.isWhitespace == true { + result.removeLast() + } + return result } // trim prefix whitespace and newlines func trim_prefix(_ str: String) -> String { - return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression) + var result = str + while result.first?.isWhitespace == true { + result.removeFirst() + } + return result } struct LongformContent { diff --git a/damus/Models/Wallet.swift b/damus/Models/Wallet.swift @@ -83,8 +83,10 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable { return .init(index: 9, tag: "breez", displayName: "Breez", link: "breez:", appStoreLink: "https://apps.apple.com/us/app/breez-lightning-client-pos/id1463604142", image: "breez") case .bitcoinbeach: - return .init(index: 10, tag: "bitcoinbeach", displayName: "Bitcoin Beach", link: "bitcoinbeach://", - appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw") + // Blink used to be called Bitcoin Beach. + // We have to keep the tag called "bitcoinbeach" for backwards compatibility. + return .init(index: 10, tag: "bitcoinbeach", displayName: "Blink", link: "blink://", + appStoreLink: "https://apps.apple.com/app/blink-bitcoin-wallet/id1531383905", image: "blink") case .blixtwallet: return .init(index: 11, tag: "blixtwallet", displayName: "Blixt Wallet", link: "blixtwallet:lightning:", appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet") diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -35,6 +35,7 @@ class Profiles { @MainActor private var profiles: [Pubkey: ProfileData] = [:] + // Map of validated NIP-05 address to pubkey. @MainActor var nip05_pubkey: [String: Pubkey] = [:] diff --git a/damus/TestData.swift b/damus/TestData.swift @@ -106,7 +106,8 @@ var test_damus_state: DamusState = ({ video: .init(), ndb: ndb, quote_reposts: .init(our_pubkey: our_pubkey), - emoji_provider: DefaultEmojiProvider(showAllVariations: true) + emoji_provider: DefaultEmojiProvider(showAllVariations: true), + favicon_cache: .init() ) /* diff --git a/damus/Util/Extensions/KFOptionSetter+.swift b/damus/Util/Extensions/KFOptionSetter+.swift @@ -29,15 +29,15 @@ extension KFOptionSetter { options.onlyLoadFirstFrame = disable_animation switch imageContext { - case .pfp: - options.diskCacheExpiration = .days(60) - break - case .banner: - options.diskCacheExpiration = .days(5) - break - case .note: - options.diskCacheExpiration = .days(1) - break + case .pfp, .favicon: + options.diskCacheExpiration = .days(60) + break + case .banner: + options.diskCacheExpiration = .days(5) + break + case .note: + options.diskCacheExpiration = .days(1) + break } return self @@ -82,11 +82,14 @@ enum ImageContext { case pfp case banner case note - + case favicon + func maxMebibyteSize() -> Int { switch self { + case .favicon: + return 512_000 // 500KiB case .pfp: - return 5_242_880 // 5Mib + return 5_242_880 // 5MiB case .banner, .note: return 20_971_520 // 20MiB } @@ -94,6 +97,8 @@ enum ImageContext { func downsampleSize() -> CGSize { switch self { + case .favicon: + return CGSize(width: 18, height: 18) case .pfp: return CGSize(width: 200, height: 200) case .banner: diff --git a/damus/Util/FaviconCache.swift b/damus/Util/FaviconCache.swift @@ -0,0 +1,41 @@ +// +// FaviconCache.swift +// damus +// +// Created by Terry Yiu on 5/23/25. +// + +import Foundation +import FaviconFinder + +class FaviconCache { + private var nip05DomainFavicons: [String: [FaviconURL]] = [:] + + @MainActor + func lookup(_ domain: String) async -> [FaviconURL] { + let lowercasedDomain = domain.lowercased() + if let faviconURLs = nip05DomainFavicons[lowercasedDomain] { + return faviconURLs + } + + guard let siteURL = URL(string: "https://\(lowercasedDomain)"), + let faviconURLs = try? await FaviconFinder( + url: siteURL, + configuration: .init( + preferredSource: .ico, // Prefer using common favicon .ico filenames at root level to avoid scraping HTML when possible. + preferences: [ + .html: FaviconFormatType.appleTouchIcon.rawValue, + .ico: "favicon.ico", + .webApplicationManifestFile: FaviconFormatType.launcherIcon4x.rawValue + ] + ) + ).fetchFaviconURLs() + else { + return [] + } + + nip05DomainFavicons[lowercasedDomain] = faviconURLs + + return faviconURLs + } +} diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift @@ -5,6 +5,7 @@ // Created by Scott Penrose on 5/7/23. // +import FaviconFinder import SwiftUI enum Route: Hashable { @@ -46,6 +47,8 @@ enum Route: Hashable { case Wallet(wallet: WalletModel) case WalletScanner(result: Binding<WalletScanResult>) case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel) + case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?) + case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey]) @ViewBuilder func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View { @@ -127,6 +130,10 @@ enum Route: Hashable { FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers) case .Script(let load_model): LoadScript(pool: damusState.nostrNetwork.pool, model: load_model) + case .NIP05DomainEvents(let events, let nip05_domain_favicon): + NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon) + case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys): + NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys) } } @@ -231,6 +238,12 @@ enum Route: Hashable { case .Script(let model): hasher.combine("script") hasher.combine(model.data.count) + case .NIP05DomainEvents(let events, _): + hasher.combine("nip05DomainEvents") + hasher.combine(events.domain) + case .NIP05DomainPubkeys(let domain, _, _): + hasher.combine("nip05DomainPubkeys") + hasher.combine(domain) } } } diff --git a/damus/Views/NIP05DomainPubkeysView.swift b/damus/Views/NIP05DomainPubkeysView.swift @@ -0,0 +1,51 @@ +// +// NIP05DomainPubkeysView.swift +// damus +// +// Created by Terry Yiu on 5/23/25. +// + +import FaviconFinder +import Kingfisher +import SwiftUI + +struct NIP05DomainPubkeysView: View { + let damus_state: DamusState + let domain: String + let nip05_domain_favicon: FaviconURL? + let pubkeys: [Pubkey] + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(pubkeys, id: \.self) { pk in + FollowUserView(target: .pubkey(pk), damus_state: damus_state) + } + } + .padding(.horizontal) + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + HStack { + if let nip05_domain_favicon { + KFImage(nip05_domain_favicon.source) + .imageContext(.favicon, disable_animation: true) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .clipped() + } + Text(domain) + .font(.headline) + } + } + } + } +} + +#Preview { + let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico) + let pubkeys = [test_pubkey, test_pubkey_2] + NIP05DomainPubkeysView(damus_state: test_damus_state, domain: "damus.io", nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys) +} diff --git a/damus/Views/NIP05DomainTimelineHeaderView.swift b/damus/Views/NIP05DomainTimelineHeaderView.swift @@ -0,0 +1,111 @@ +// +// NIP05DomainTimelineHeaderView.swift +// damus +// +// Created by Terry Yiu on 5/16/25. +// + +import FaviconFinder +import Kingfisher +import SwiftUI + +struct NIP05DomainTimelineHeaderView: View { + let damus_state: DamusState + @ObservedObject var model: NIP05DomainEventsModel + let nip05_domain_favicon: FaviconURL? + + @Environment(\.openURL) var openURL + + var Icon: some View { + ZStack { + if let nip05_domain_favicon { + KFImage(nip05_domain_favicon.source) + .imageContext(.favicon, disable_animation: true) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .clipped() + } else { + EmptyView() + } + } + } + + var friendsOfFriends: [Pubkey] { + // Order it such that the pubkeys that have events come first in the array so that their profile pictures + // show first. + let pubkeys = model.events.all_events.map { $0.pubkey } + (model.filter.authors ?? []) + + // Filter out duplicates but retain order, and filter out any that do not have a validated NIP-05. + return (NSMutableOrderedSet(array: pubkeys).array as? [Pubkey] ?? []) + .filter { + damus_state.contacts.is_in_friendosphere($0) && damus_state.profiles.is_validated($0) != nil + } + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + if nip05_domain_favicon != nil { + Icon + } + + Text(model.domain) + .foregroundStyle(DamusLogoGradient.gradient) + .font(.title.bold()) + .onTapGesture { + if let url = URL(string: "https://\(model.domain)") { + openURL(url) + } + } + } + + let friendsOfFriends = friendsOfFriends + + HStack { + CondensedProfilePicturesView(state: damus_state, pubkeys: friendsOfFriends, maxPictures: 3) + let friendsOfFriendsString = friendsOfFriendsString(friendsOfFriends, ndb: damus_state.ndb) + Text(friendsOfFriendsString) + .font(.subheadline) + .foregroundColor(.gray) + .multilineTextAlignment(.leading) + } + .onTapGesture { + if !friendsOfFriends.isEmpty { + damus_state.nav.push(route: Route.NIP05DomainPubkeys(domain: model.domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: friendsOfFriends)) + } + } + } + } +} + +func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + let names: [String] = friendsOfFriends.prefix(3).map { pk in + let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile + return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) + } + + switch friendsOfFriends.count { + case 0: + return "No one in your trusted network is associated with this domain." + case 1: + let format = NSLocalizedString("Notes from %@", bundle: bundle, comment: "Text to indicate that notes from one pubkey in our trusted network are shown below.") + return String(format: format, locale: locale, names[0]) + case 2: + let format = NSLocalizedString("Notes from %@ & %@", bundle: bundle, comment: "Text to indicate that notes from two pubkeys in our trusted network are shown below.") + return String(format: format, locale: locale, names[0], names[1]) + case 3: + let format = NSLocalizedString("Notes from %@, %@ & %@", bundle: bundle, comment: "Text to indicate that notes from three pubkeys in our trusted network are shown below.") + return String(format: format, locale: locale, names[0], names[1], names[2]) + default: + let format = localizedStringFormat(key: "notes_from_three_and_others", locale: locale) + return String(format: format, locale: locale, friendsOfFriends.count - 3, names[0], names[1], names[2]) + } +} + +#Preview { + let model = NIP05DomainEventsModel(state: test_damus_state, domain: "damus.io") + let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico) + NIP05DomainTimelineHeaderView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon) +} diff --git a/damus/Views/NIP05DomainTimelineView.swift b/damus/Views/NIP05DomainTimelineView.swift @@ -0,0 +1,64 @@ +// +// NIP05DomainTimelineView.swift +// damus +// +// Created by Terry Yiu on 4/11/25. +// + +import FaviconFinder +import Kingfisher +import SwiftUI + +struct NIP05DomainTimelineView: View { + let damus_state: DamusState + @ObservedObject var model: NIP05DomainEventsModel + let nip05_domain_favicon: FaviconURL? + + func nip05_filter(ev: NostrEvent) -> Bool { + damus_state.contacts.is_in_friendosphere(ev.pubkey) && damus_state.profiles.is_validated(ev.pubkey) != nil + } + + var contentFilters: ContentFilters { + var filters = Array<(NostrEvent) -> Bool>() + filters.append(contentsOf: ContentFilters.defaults(damus_state: damus_state)) + filters.append(nip05_filter) + return ContentFilters(filters: filters) + } + + var body: some View { + let height: CGFloat = 250.0 + + TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: contentFilters.filter(ev:)) { + ZStack(alignment: .leading) { + DamusBackground(maxHeight: height) + .mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom)) + NIP05DomainTimelineHeaderView(damus_state: damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon) + .padding(.leading, 30) + .padding(.top, 30) + } + } + .ignoresSafeArea() + .padding(.bottom, tabHeight) + .onAppear { + guard model.events.all_events.isEmpty else { return } + + model.subscribe() + + if let pubkeys = model.filter.authors { + for pubkey in pubkeys { + check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles) + } + } + } + .onDisappear { + model.unsubscribe() + } + } +} + +#Preview { + let damus_state = test_damus_state + let model = NIP05DomainEventsModel(state: damus_state, domain: "damus.io") + let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico) + NIP05DomainTimelineView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon) +} diff --git a/damus/Views/Profile/ProfileName.swift b/damus/Views/Profile/ProfileName.swift @@ -5,6 +5,7 @@ // Created by William Casarin on 2022-04-16. // +import FaviconFinder import SwiftUI enum FriendType { @@ -43,6 +44,7 @@ struct ProfileName: View { @State var nip05: NIP05? @State var donation: Int? @State var purple_account: DamusPurple.Account? + @State var nip05_domain_favicon: FaviconURL? init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) { self.pubkey = pubkey @@ -61,7 +63,7 @@ struct ProfileName: View { var current_nip05: NIP05? { nip05 ?? damus_state.profiles.is_validated(pubkey) } - + func current_display_name(profile: Profile?) -> DisplayName { return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey) } @@ -101,7 +103,7 @@ struct ProfileName: View { .fontWeight(prefix == "@" ? .none : .bold) if let nip05 = current_nip05 { - NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles) + NIP05Badge(nip05: nip05, pubkey: pubkey, damus_state: damus_state, show_domain: show_nip5_domain, nip05_domain_favicon: nip05_domain_favicon) } if let friend = friend_type, current_nip05 == nil { @@ -118,9 +120,15 @@ struct ProfileName: View { } .task { - if damus_state.purple.enable_purple { - self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey) - } + if damus_state.purple.enable_purple { + self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey) + } + } + .task { + if let domain = current_nip05?.host { + self.nip05_domain_favicon = try? await damus_state.favicon_cache.lookup(domain) + .largest() + } } .onReceive(handle_notify(.profile_updated)) { update in if update.pubkey != pubkey { @@ -151,6 +159,24 @@ struct ProfileName: View { let nip05 = damus_state.profiles.is_validated(pubkey) if nip05 != self.nip05 { self.nip05 = nip05 + + if let domain = nip05?.host { + Task { + let favicon = try? await damus_state.favicon_cache.lookup(domain) + .filter { + if let size = $0.size { + return size.width <= 128 && size.height <= 128 + } else { + return true + } + } + .largest() + + await MainActor.run { + self.nip05_domain_favicon = favicon + } + } + } } if donation != profile.damus_donation { diff --git a/damus/Views/Reposts/QuoteRepostsView.swift b/damus/Views/Reposts/QuoteRepostsView.swift @@ -12,9 +12,19 @@ struct QuoteRepostsView: View { @ObservedObject var model: EventsModel var body: some View { - TimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:)) + TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:)) { + ZStack(alignment: .leading) { + DamusBackground(maxHeight: 250) + .mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom)) + Text("Quotes", comment: "Navigation bar title for Quote Reposts view.") + .foregroundStyle(DamusLogoGradient.gradient) + .font(.title.bold()) + .padding(.leading, 30) + .padding(.top, 30) + } + } + .ignoresSafeArea() .padding(.bottom, tabHeight) - .navigationBarTitle(NSLocalizedString("Quotes", comment: "Navigation bar title for Quote Reposts view.")) .onAppear { model.subscribe() } diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings Binary files differ. diff --git a/damus/de.lproj/Localizable.stringsdict b/damus/de.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ <string>Folge ich</string> </dict> </dict> + <key>hellthread_notifications_disabled</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@HELLTHREAD_PROFILES@</string> + <key>HELLTHREAD_PROFILES</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Benachrichtigungen ausblenden, die mehr als %d Profil markieren</string> + <key>other</key> + <string>Benachrichtigungen ausblenden, die mehr als %d Profile markieren.</string> + </dict> + </dict> <key>imports_count</key> <dict> <key>NSStringLocalizedFormatKey</key> @@ -82,6 +98,22 @@ <string>%2$@ und %1$d weitere teilten</string> </dict> </dict> + <key>quoted_reposts_count</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@QUOTE_REPOSTS@</string> + <key>QUOTE_REPOSTS</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Zitate</string> + <key>other</key> + <string>Zitat</string> + </dict> + </dict> <key>reacted_tagged_in_3</key> <dict> <key>NSStringLocalizedFormatKey</key> @@ -242,22 +274,6 @@ <string>geteilte Beiträge</string> </dict> </dict> - <key>quoted_reposts_count</key> - <dict> - <key>NSStringLocalizedFormatKey</key> - <string>%#@QUOTE_REPOSTS@</string> - <key>QUOTE_REPOSTS</key> - <dict> - <key>NSStringFormatSpecTypeKey</key> - <string>NSStringPluralRuleType</string> - <key>NSStringFormatValueTypeKey</key> - <string>d</string> - <key>one</key> - <string>Zitate</string> - <key>other</key> - <string>Zitat</string> - </dict> - </dict> <key>sats</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict @@ -82,6 +82,22 @@ <string>Imports</string> </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>Notes from %2$@, %3$@, %4$@ & %1$d other in your trusted network</string> + <key>other</key> + <string>Notes from %2$@, %3$@, %4$@ & %1$d others in your trusted network</string> + </dict> + </dict> <key>people_reposted_count</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git a/damus/en-US.xcloc/Localized Contents/en-US.xliff b/damus/en-US.xcloc/Localized Contents/en-US.xliff @@ -308,6 +308,11 @@ Label for filter for all notifications.</note> <target>An additional percentage of each zap will be sent to support Damus development</target> <note>Text indicating that they can contribute zaps to support Damus development.</note> </trans-unit> + <trans-unit id="An internal error occurred in your wallet." xml:space="preserve"> + <source>An internal error occurred in your wallet.</source> + <target>An internal error occurred in your wallet.</target> + <note>Error description for an internal error</note> + </trans-unit> <trans-unit id="An unexpected error happened while trying to perform this action. Please contact support." xml:space="preserve"> <source>An unexpected error happened while trying to perform this action. Please contact support.</source> <target>An unexpected error happened while trying to perform this action. Please contact support.</target> @@ -323,6 +328,11 @@ Label for filter for all notifications.</note> <target>An unknown error occurred while adding a relay.</target> <note>Title of an unknown relay error message.</note> </trans-unit> + <trans-unit id="An unspecified error occurred in your wallet." xml:space="preserve"> + <source>An unspecified error occurred in your wallet.</source> + <target>An unspecified error occurred in your wallet.</target> + <note>Error description for an unspecified error</note> + </trans-unit> <trans-unit id="Animations" xml:space="preserve"> <source>Animations</source> <target>Animations</target> @@ -511,6 +521,11 @@ Text for button to cancel out of connecting Nostr Wallet Connect lightning walle <target>Check the address and/or the relay list.</target> <note>Human readable tip for error</note> </trans-unit> + <trans-unit id="Check your account permissions or contact support." xml:space="preserve"> + <source>Check your account permissions or contact support.</source> + <target>Check your account permissions or contact support.</target> + <note>Tip for restricted operation</note> + </trans-unit> <trans-unit id="Check your internet connection and try again. If the error persists, contact support." xml:space="preserve"> <source>Check your internet connection and try again. If the error persists, contact support.</source> <target>Check your internet connection and try again. If the error persists, contact support.</target> @@ -557,9 +572,9 @@ Text for button to cancel out of connecting Nostr Wallet Connect lightning walle <note>Button label giving the user the option to close the sheet from which they posted a highlight Button label giving the user the option to close the sheet from which they were trying to post a highlight</note> </trans-unit> - <trans-unit id="Coinos is a service operated by a third-party. We have no access to your Coinos wallet." xml:space="preserve"> - <source>Coinos is a service operated by a third-party. We have no access to your Coinos wallet.</source> - <target>Coinos is a service operated by a third-party. We have no access to your Coinos wallet.</target> + <trans-unit id="Coinos is a service operated by a third-party. The Damus team has no access to your wallet." xml:space="preserve"> + <source>Coinos is a service operated by a third-party. The Damus team has no access to your wallet.</source> + <target>Coinos is a service operated by a third-party. The Damus team has no access to your wallet.</target> <note>Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service.</note> </trans-unit> <trans-unit id="Coming soon" xml:space="preserve"> @@ -652,6 +667,11 @@ Prompt to user to continue</note> <target>Copied</target> <note>Label indicating that a user's key was copied.</note> </trans-unit> + <trans-unit id="Copied!" xml:space="preserve"> + <source>Copied!</source> + <target>Copied!</target> + <note>Label indicating that the error technical information was successfully copied to the clipboard, which shows up as soon as the user clicks the copy button.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Copy</target> @@ -709,6 +729,11 @@ Context menu option for copying the version of damus.</note> <target>Copy note JSON</target> <note>Context menu option for copying the JSON text from the note.</note> </trans-unit> + <trans-unit id="Copy technical information" xml:space="preserve"> + <source>Copy technical information</source> + <target>Copy technical information</target> + <note>Button label to allow user to copy technical information from an error screen (usually to provide our support team for further troubleshooting)</note> + </trans-unit> <trans-unit id="Copy text" xml:space="preserve"> <source>Copy text</source> <target>Copy text</target> @@ -855,6 +880,11 @@ Section header for developer settings</note> <target>Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode.</target> <note>Section header for Developer Settings view</note> </trans-unit> + <trans-unit id="Disable high balance warning" xml:space="preserve"> + <source>Disable high balance warning</source> + <target>Disable high balance warning</target> + <note>Setting to disable high balance warnings on the user's wallet</note> + </trans-unit> <trans-unit id="Discard changes?" xml:space="preserve"> <source>Discard changes?</source> <target>Discard changes?</target> @@ -874,7 +904,8 @@ Button to disconnect from the relay.</note> <trans-unit id="Dismiss" xml:space="preserve"> <source>Dismiss</source> <target>Dismiss</target> - <note>Button to dismiss alert + <note>Button label to dismiss the safety reminder that the user's wallet has a high balance +Button to dismiss alert Button to dismiss error</note> </trans-unit> <trans-unit id="Done" xml:space="preserve"> @@ -926,6 +957,11 @@ Edit Button for editing profile</note> <target>Edit profile picture</target> <note>Accessibility label for a button that edits a profile picture</note> </trans-unit> + <trans-unit id="Empty error message" xml:space="preserve"> + <source>Empty error message</source> + <target>Empty error message</target> + <note>A human readable placeholder to indicate that the error message is empty</note> + </trans-unit> <trans-unit id="Enable Purple auto-translations" xml:space="preserve"> <source>Enable Purple auto-translations</source> <target>Enable Purple auto-translations</target> @@ -1620,6 +1656,11 @@ Text label indicating that there is no NIP-11 relay description information foun Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable. Text label indicating that there is no NIP-11 relay software version information found. In English, N/A stands for not applicable.</note> </trans-unit> + <trans-unit id="NWC wallet" xml:space="preserve"> + <source>NWC wallet</source> + <target>NWC wallet</target> + <note>Title for section in zap settings that controls general NWC wallet settings.</note> + </trans-unit> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Name</target> @@ -1942,6 +1983,11 @@ Section title for deleting the user</note> <target>Plan</target> <note>Prompt selection of DeepL subscription plan to perform machine translations on notes</note> </trans-unit> + <trans-unit id="Please check for updates or contact your wallet provider." xml:space="preserve"> + <source>Please check for updates or contact your wallet provider.</source> + <target>Please check for updates or contact your wallet provider.</target> + <note>Tip for not implemented error</note> + </trans-unit> <trans-unit id="Please check the address and try again" xml:space="preserve"> <source>Please check the address and try again</source> <target>Please check the address and try again</target> @@ -1962,11 +2008,26 @@ Section title for deleting the user</note> <target>Please contact support.</target> <note>Tip for an unknown relay error message.</note> </trans-unit> + <trans-unit id="Please contact the developer of your wallet provider for help." xml:space="preserve"> + <source>Please contact the developer of your wallet provider for help.</source> + <target>Please contact the developer of your wallet provider for help.</target> + <note>Human readable error description for an unknown error raised by a wallet provider.</note> + </trans-unit> <trans-unit id="Please contact the person who provided the link, and ask for another link." xml:space="preserve"> <source>Please contact the person who provided the link, and ask for another link.</source> <target>Please contact the person who provided the link, and ask for another link.</target> <note>User-visible tip on what to do if a link contains a deprecated "nrelay" reference.</note> </trans-unit> + <trans-unit id="Please copy the technical info and send it to our support team." xml:space="preserve"> + <source>Please copy the technical info and send it to our support team.</source> + <target>Please copy the technical info and send it to our support team.</target> + <note>Tip on how to resolve issue when wallet returns an invalid response</note> + </trans-unit> + <trans-unit id="Please deposit more funds and try again." xml:space="preserve"> + <source>Please deposit more funds and try again.</source> + <target>Please deposit more funds and try again.</target> + <note>Tip for insufficient balance errors</note> + </trans-unit> <trans-unit id="Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." xml:space="preserve"> <source>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</source> <target>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</target> @@ -1987,6 +2048,11 @@ Section title for deleting the user</note> <target>Please try again later or contact support if the issue persists.</target> <note>Human readable tip for error</note> </trans-unit> + <trans-unit id="Please try again or contact your wallet provider for further assistance." xml:space="preserve"> + <source>Please try again or contact your wallet provider for further assistance.</source> + <target>Please try again or contact your wallet provider for further assistance.</target> + <note>Tip for unspecified error</note> + </trans-unit> <trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve"> <source>Please try again, check the URL for typos, or contact support for further help.</source> <target>Please try again, check the URL for typos, or contact support for further help.</target> @@ -1997,6 +2063,11 @@ Section title for deleting the user</note> <target>Please try opening this content on another Nostr app that supports this type of content.</target> <note>User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.</note> </trans-unit> + <trans-unit id="Please verify your credentials or permissions." xml:space="preserve"> + <source>Please verify your credentials or permissions.</source> + <target>Please verify your credentials or permissions.</target> + <note>Tip for unauthorized access</note> + </trans-unit> <trans-unit id="Point your camera to a QR code…" xml:space="preserve"> <source>Point your camera to a QR code…</source> <target>Point your camera to a QR code…</target> @@ -2810,6 +2881,11 @@ Enjoy!</target> <target>This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.</target> <note>Notice label that user cannot manage their In-App purchases</note> </trans-unit> + <trans-unit id="This feature is not implemented by your wallet." xml:space="preserve"> + <source>This feature is not implemented by your wallet.</source> + <target>This feature is not implemented by your wallet.</target> + <note>Error description for not implemented feature</note> + </trans-unit> <trans-unit id="This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve"> <source>This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</source> <target>This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</target> @@ -2839,6 +2915,11 @@ Nice to meet you all! #introductions #plebchain </target> <target>This note contains too many items and cannot be rendered</target> <note>Error message indicating that a note is too big and cannot be rendered</note> </trans-unit> + <trans-unit id="This operation is restricted by your wallet." xml:space="preserve"> + <source>This operation is restricted by your wallet.</source> + <target>This operation is restricted by your wallet.</target> + <note>Error description for restricted operation</note> + </trans-unit> <trans-unit id="This relay is already in your list." xml:space="preserve"> <source>This relay is already in your list.</source> <target>This relay is already in your list.</target> @@ -2915,6 +2996,11 @@ Section header for text and appearance settings</note> <target>Try checking the link again, your internet connection, or contact the person who provided you the link for help.</target> <note>Tips on what to do if a note cannot be found.</note> </trans-unit> + <trans-unit id="Try restarting your wallet or contacting support if the problem persists." xml:space="preserve"> + <source>Try restarting your wallet or contacting support if the problem persists.</source> + <target>Try restarting your wallet or contacting support if the problem persists.</target> + <note>Tip for internal error</note> + </trans-unit> <trans-unit id="Type %@ to delete" xml:space="preserve"> <source>Type %@ to delete</source> <target>Type %@ to delete</target> @@ -3107,6 +3193,16 @@ This will reset your contact list, including the list of everyone you follow and This will reset your contact list, including the list of everyone you follow and potentially the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</target> <note>Alert for resetting the user's contact list.</note> </trans-unit> + <trans-unit id="Wait a few moments, and then try again." xml:space="preserve"> + <source>Wait a few moments, and then try again.</source> + <target>Wait a few moments, and then try again.</target> + <note>Tip for rate limit error</note> + </trans-unit> + <trans-unit id="Wait for the quota to reset, or configure your wallet provider to allow a higher limit." xml:space="preserve"> + <source>Wait for the quota to reset, or configure your wallet provider to allow a higher limit.</source> + <target>Wait for the quota to reset, or configure your wallet provider to allow a higher limit.</target> + <note>Tip for quota exceeded</note> + </trans-unit> <trans-unit id="Wallet" xml:space="preserve"> <source>Wallet</source> <target>Wallet</target> @@ -3115,6 +3211,21 @@ Navigation title for attaching Nostr Wallet Connect lightning wallet. Sidebar menu label for Wallet view. Title for section in zap settings that controls the Lightning wallet selection.</note> </trans-unit> + <trans-unit id="Wallet provider returned a response that we could not decrypt." xml:space="preserve"> + <source>Wallet provider returned a response that we could not decrypt.</source> + <target>Wallet provider returned a response that we could not decrypt.</target> + <note>Error description shown to the user when a response from the wallet provider contains data the app could not decrypt.</note> + </trans-unit> + <trans-unit id="Wallet provider returned a response that we do not understand." xml:space="preserve"> + <source>Wallet provider returned a response that we do not understand.</source> + <target>Wallet provider returned a response that we do not understand.</target> + <note>Error description shown to the user when a response from the wallet provider contains data the app does not understand</note> + </trans-unit> + <trans-unit id="Wallet provider returned an invalid response." xml:space="preserve"> + <source>Wallet provider returned an invalid response.</source> + <target>Wallet provider returned an invalid response.</target> + <note>Error description shown to the user when a response from the wallet provider is invalid</note> + </trans-unit> <trans-unit id="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)" xml:space="preserve"> <source>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)</source> <target>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)</target> @@ -3206,6 +3317,11 @@ User confirm Yes</note> <target>Yes, Overwrite</target> <note>Text of button that confirms to overwrite the existing mutelist.</note> </trans-unit> + <trans-unit id="You are not authorized to perform this action with your wallet." xml:space="preserve"> + <source>You are not authorized to perform this action with your wallet.</source> + <target>You are not authorized to perform this action with your wallet.</target> + <note>Error description for unauthorized access</note> + </trans-unit> <trans-unit id="You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again." xml:space="preserve"> <source>You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.</source> <target>You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.</target> @@ -3276,6 +3392,11 @@ User confirm Yes</note> <target>Your Purple subscription has expired. Renew?</target> <note>A notification message explaining to the user that their Damus Purple Subscription has expired, prompting them to renew.</note> </trans-unit> + <trans-unit id="Your connected wallet raised an unknown error. Message: %s" xml:space="preserve"> + <source>Your connected wallet raised an unknown error. Message: %s</source> + <target>Your connected wallet raised an unknown error. Message: %s</target> + <note>Human readable error description for unknown error</note> + </trans-unit> <trans-unit id="Your draft has been saved to storage." xml:space="preserve"> <source>Your draft has been saved to storage.</source> <target>Your draft has been saved to storage.</target> @@ -3301,6 +3422,21 @@ User confirm Yes</note> <target>Your report will be sent to the relays you are connected to</target> <note>Footer text to inform user what will happen when the report is submitted.</note> </trans-unit> + <trans-unit id="Your transaction quota has been exceeded." xml:space="preserve"> + <source>Your transaction quota has been exceeded.</source> + <target>Your transaction quota has been exceeded.</target> + <note>Error description for quota exceeded</note> + </trans-unit> + <trans-unit id="Your wallet does not have sufficient balance for this transaction." xml:space="preserve"> + <source>Your wallet does not have sufficient balance for this transaction.</source> + <target>Your wallet does not have sufficient balance for this transaction.</target> + <note>Error description for insufficient balance</note> + </trans-unit> + <trans-unit id="Your wallet is temporarily being rate limited." xml:space="preserve"> + <source>Your wallet is temporarily being rate limited.</source> + <target>Your wallet is temporarily being rate limited.</target> + <note>Error description for rate limit error</note> + </trans-unit> <trans-unit id="Zap" xml:space="preserve"> <source>Zap</source> <target>Zap</target> @@ -4287,6 +4423,11 @@ Label for filter for all notifications.</note> <target state="new">An additional percentage of each zap will be sent to support Damus development</target> <note>Text indicating that they can contribute zaps to support Damus development.</note> </trans-unit> + <trans-unit id="An internal error occurred in your wallet." xml:space="preserve"> + <source>An internal error occurred in your wallet.</source> + <target state="new">An internal error occurred in your wallet.</target> + <note>Error description for an internal error</note> + </trans-unit> <trans-unit id="An unexpected error happened while trying to perform this action. Please contact support." xml:space="preserve"> <source>An unexpected error happened while trying to perform this action. Please contact support.</source> <target state="new">An unexpected error happened while trying to perform this action. Please contact support.</target> @@ -4302,6 +4443,11 @@ Label for filter for all notifications.</note> <target state="new">An unknown error occurred while adding a relay.</target> <note>Title of an unknown relay error message.</note> </trans-unit> + <trans-unit id="An unspecified error occurred in your wallet." xml:space="preserve"> + <source>An unspecified error occurred in your wallet.</source> + <target state="new">An unspecified error occurred in your wallet.</target> + <note>Error description for an unspecified error</note> + </trans-unit> <trans-unit id="Animations" xml:space="preserve"> <source>Animations</source> <target state="new">Animations</target> @@ -4490,6 +4636,11 @@ Text for button to cancel out of connecting Nostr Wallet Connect lightning walle <target state="new">Check the address and/or the relay list.</target> <note>Human readable tip for error</note> </trans-unit> + <trans-unit id="Check your account permissions or contact support." xml:space="preserve"> + <source>Check your account permissions or contact support.</source> + <target state="new">Check your account permissions or contact support.</target> + <note>Tip for restricted operation</note> + </trans-unit> <trans-unit id="Check your internet connection and try again. If the error persists, contact support." xml:space="preserve"> <source>Check your internet connection and try again. If the error persists, contact support.</source> <target state="new">Check your internet connection and try again. If the error persists, contact support.</target> @@ -4539,9 +4690,9 @@ Button label giving the user the option to close the sheet from which they were Button label giving the user the option to close the sheet from which they were trying to share. Button label giving the user the option to close the view when no content is available to share</note> </trans-unit> - <trans-unit id="Coinos is a service operated by a third-party. We have no access to your Coinos wallet." xml:space="preserve"> - <source>Coinos is a service operated by a third-party. We have no access to your Coinos wallet.</source> - <target state="new">Coinos is a service operated by a third-party. We have no access to your Coinos wallet.</target> + <trans-unit id="Coinos is a service operated by a third-party. The Damus team has no access to your wallet." xml:space="preserve"> + <source>Coinos is a service operated by a third-party. The Damus team has no access to your wallet.</source> + <target state="new">Coinos is a service operated by a third-party. The Damus team has no access to your wallet.</target> <note>Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service.</note> </trans-unit> <trans-unit id="Coming soon" xml:space="preserve"> @@ -4634,6 +4785,11 @@ Prompt to user to continue</note> <target state="new">Copied</target> <note>Label indicating that a user's key was copied.</note> </trans-unit> + <trans-unit id="Copied!" xml:space="preserve"> + <source>Copied!</source> + <target state="new">Copied!</target> + <note>Label indicating that the error technical information was successfully copied to the clipboard, which shows up as soon as the user clicks the copy button.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target state="new">Copy</target> @@ -4691,6 +4847,11 @@ Context menu option for copying the version of damus.</note> <target state="new">Copy note JSON</target> <note>Context menu option for copying the JSON text from the note.</note> </trans-unit> + <trans-unit id="Copy technical information" xml:space="preserve"> + <source>Copy technical information</source> + <target state="new">Copy technical information</target> + <note>Button label to allow user to copy technical information from an error screen (usually to provide our support team for further troubleshooting)</note> + </trans-unit> <trans-unit id="Copy text" xml:space="preserve"> <source>Copy text</source> <target state="new">Copy text</target> @@ -4837,6 +4998,11 @@ Section header for developer settings</note> <target state="new">Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode.</target> <note>Section header for Developer Settings view</note> </trans-unit> + <trans-unit id="Disable high balance warning" xml:space="preserve"> + <source>Disable high balance warning</source> + <target state="new">Disable high balance warning</target> + <note>Setting to disable high balance warnings on the user's wallet</note> + </trans-unit> <trans-unit id="Discard changes?" xml:space="preserve"> <source>Discard changes?</source> <target state="new">Discard changes?</target> @@ -4856,7 +5022,8 @@ Button to disconnect from the relay.</note> <trans-unit id="Dismiss" xml:space="preserve"> <source>Dismiss</source> <target state="new">Dismiss</target> - <note>Button to dismiss alert + <note>Button label to dismiss the safety reminder that the user's wallet has a high balance +Button to dismiss alert Button to dismiss error</note> </trans-unit> <trans-unit id="Done" xml:space="preserve"> @@ -4908,6 +5075,11 @@ Edit Button for editing profile</note> <target state="new">Edit profile picture</target> <note>Accessibility label for a button that edits a profile picture</note> </trans-unit> + <trans-unit id="Empty error message" xml:space="preserve"> + <source>Empty error message</source> + <target state="new">Empty error message</target> + <note>A human readable placeholder to indicate that the error message is empty</note> + </trans-unit> <trans-unit id="Enable Purple auto-translations" xml:space="preserve"> <source>Enable Purple auto-translations</source> <target state="new">Enable Purple auto-translations</target> @@ -5597,6 +5769,11 @@ Text label indicating that there is no NIP-11 relay description information foun Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable. Text label indicating that there is no NIP-11 relay software version information found. In English, N/A stands for not applicable.</note> </trans-unit> + <trans-unit id="NWC wallet" xml:space="preserve"> + <source>NWC wallet</source> + <target state="new">NWC wallet</target> + <note>Title for section in zap settings that controls general NWC wallet settings.</note> + </trans-unit> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target state="new">Name</target> @@ -5919,6 +6096,11 @@ Section title for deleting the user</note> <target state="new">Plan</target> <note>Prompt selection of DeepL subscription plan to perform machine translations on notes</note> </trans-unit> + <trans-unit id="Please check for updates or contact your wallet provider." xml:space="preserve"> + <source>Please check for updates or contact your wallet provider.</source> + <target state="new">Please check for updates or contact your wallet provider.</target> + <note>Tip for not implemented error</note> + </trans-unit> <trans-unit id="Please check the address and try again" xml:space="preserve"> <source>Please check the address and try again</source> <target state="new">Please check the address and try again</target> @@ -5939,11 +6121,26 @@ Section title for deleting the user</note> <target state="new">Please contact support.</target> <note>Tip for an unknown relay error message.</note> </trans-unit> + <trans-unit id="Please contact the developer of your wallet provider for help." xml:space="preserve"> + <source>Please contact the developer of your wallet provider for help.</source> + <target state="new">Please contact the developer of your wallet provider for help.</target> + <note>Human readable error description for an unknown error raised by a wallet provider.</note> + </trans-unit> <trans-unit id="Please contact the person who provided the link, and ask for another link." xml:space="preserve"> <source>Please contact the person who provided the link, and ask for another link.</source> <target state="new">Please contact the person who provided the link, and ask for another link.</target> <note>User-visible tip on what to do if a link contains a deprecated "nrelay" reference.</note> </trans-unit> + <trans-unit id="Please copy the technical info and send it to our support team." xml:space="preserve"> + <source>Please copy the technical info and send it to our support team.</source> + <target state="new">Please copy the technical info and send it to our support team.</target> + <note>Tip on how to resolve issue when wallet returns an invalid response</note> + </trans-unit> + <trans-unit id="Please deposit more funds and try again." xml:space="preserve"> + <source>Please deposit more funds and try again.</source> + <target state="new">Please deposit more funds and try again.</target> + <note>Tip for insufficient balance errors</note> + </trans-unit> <trans-unit id="Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." xml:space="preserve"> <source>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</source> <target state="new">Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</target> @@ -5964,6 +6161,11 @@ Section title for deleting the user</note> <target state="new">Please try again later or contact support if the issue persists.</target> <note>Human readable tip for error</note> </trans-unit> + <trans-unit id="Please try again or contact your wallet provider for further assistance." xml:space="preserve"> + <source>Please try again or contact your wallet provider for further assistance.</source> + <target state="new">Please try again or contact your wallet provider for further assistance.</target> + <note>Tip for unspecified error</note> + </trans-unit> <trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve"> <source>Please try again, check the URL for typos, or contact support for further help.</source> <target state="new">Please try again, check the URL for typos, or contact support for further help.</target> @@ -5974,6 +6176,11 @@ Section title for deleting the user</note> <target state="new">Please try opening this content on another Nostr app that supports this type of content.</target> <note>User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.</note> </trans-unit> + <trans-unit id="Please verify your credentials or permissions." xml:space="preserve"> + <source>Please verify your credentials or permissions.</source> + <target state="new">Please verify your credentials or permissions.</target> + <note>Tip for unauthorized access</note> + </trans-unit> <trans-unit id="Point your camera to a QR code…" xml:space="preserve"> <source>Point your camera to a QR code…</source> <target state="new">Point your camera to a QR code…</target> @@ -6792,6 +6999,11 @@ Enjoy!</target> <target state="new">This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.</target> <note>Notice label that user cannot manage their In-App purchases</note> </trans-unit> + <trans-unit id="This feature is not implemented by your wallet." xml:space="preserve"> + <source>This feature is not implemented by your wallet.</source> + <target state="new">This feature is not implemented by your wallet.</target> + <note>Error description for not implemented feature</note> + </trans-unit> <trans-unit id="This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve"> <source>This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</source> <target state="new">This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</target> @@ -6816,6 +7028,11 @@ Nice to meet you all! #introductions #plebchain </target> <target state="new">This note contains too many items and cannot be rendered</target> <note>Error message indicating that a note is too big and cannot be rendered</note> </trans-unit> + <trans-unit id="This operation is restricted by your wallet." xml:space="preserve"> + <source>This operation is restricted by your wallet.</source> + <target state="new">This operation is restricted by your wallet.</target> + <note>Error description for restricted operation</note> + </trans-unit> <trans-unit id="This relay is already in your list." xml:space="preserve"> <source>This relay is already in your list.</source> <target state="new">This relay is already in your list.</target> @@ -6892,6 +7109,11 @@ Section header for text and appearance settings</note> <target state="new">Try checking the link again, your internet connection, or contact the person who provided you the link for help.</target> <note>Tips on what to do if a note cannot be found.</note> </trans-unit> + <trans-unit id="Try restarting your wallet or contacting support if the problem persists." xml:space="preserve"> + <source>Try restarting your wallet or contacting support if the problem persists.</source> + <target state="new">Try restarting your wallet or contacting support if the problem persists.</target> + <note>Tip for internal error</note> + </trans-unit> <trans-unit id="Type %@ to delete" xml:space="preserve"> <source>Type %@ to delete</source> <target state="new">Type %@ to delete</target> @@ -7084,6 +7306,16 @@ This will reset your contact list, including the list of everyone you follow and This will reset your contact list, including the list of everyone you follow and potentially the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</target> <note>Alert for resetting the user's contact list.</note> </trans-unit> + <trans-unit id="Wait a few moments, and then try again." xml:space="preserve"> + <source>Wait a few moments, and then try again.</source> + <target state="new">Wait a few moments, and then try again.</target> + <note>Tip for rate limit error</note> + </trans-unit> + <trans-unit id="Wait for the quota to reset, or configure your wallet provider to allow a higher limit." xml:space="preserve"> + <source>Wait for the quota to reset, or configure your wallet provider to allow a higher limit.</source> + <target state="new">Wait for the quota to reset, or configure your wallet provider to allow a higher limit.</target> + <note>Tip for quota exceeded</note> + </trans-unit> <trans-unit id="Wallet" xml:space="preserve"> <source>Wallet</source> <target state="new">Wallet</target> @@ -7092,6 +7324,21 @@ Navigation title for attaching Nostr Wallet Connect lightning wallet. Sidebar menu label for Wallet view. Title for section in zap settings that controls the Lightning wallet selection.</note> </trans-unit> + <trans-unit id="Wallet provider returned a response that we could not decrypt." xml:space="preserve"> + <source>Wallet provider returned a response that we could not decrypt.</source> + <target state="new">Wallet provider returned a response that we could not decrypt.</target> + <note>Error description shown to the user when a response from the wallet provider contains data the app could not decrypt.</note> + </trans-unit> + <trans-unit id="Wallet provider returned a response that we do not understand." xml:space="preserve"> + <source>Wallet provider returned a response that we do not understand.</source> + <target state="new">Wallet provider returned a response that we do not understand.</target> + <note>Error description shown to the user when a response from the wallet provider contains data the app does not understand</note> + </trans-unit> + <trans-unit id="Wallet provider returned an invalid response." xml:space="preserve"> + <source>Wallet provider returned an invalid response.</source> + <target state="new">Wallet provider returned an invalid response.</target> + <note>Error description shown to the user when a response from the wallet provider is invalid</note> + </trans-unit> <trans-unit id="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)" xml:space="preserve"> <source>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)</source> <target state="new">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)</target> @@ -7183,6 +7430,11 @@ User confirm Yes</note> <target state="new">Yes, Overwrite</target> <note>Text of button that confirms to overwrite the existing mutelist.</note> </trans-unit> + <trans-unit id="You are not authorized to perform this action with your wallet." xml:space="preserve"> + <source>You are not authorized to perform this action with your wallet.</source> + <target state="new">You are not authorized to perform this action with your wallet.</target> + <note>Error description for unauthorized access</note> + </trans-unit> <trans-unit id="You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." xml:space="preserve"> <source>You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.</source> <target state="new">You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.</target> @@ -7248,6 +7500,11 @@ User confirm Yes</note> <target state="new">Your Purple subscription has expired. Renew?</target> <note>A notification message explaining to the user that their Damus Purple Subscription has expired, prompting them to renew.</note> </trans-unit> + <trans-unit id="Your connected wallet raised an unknown error. Message: %s" xml:space="preserve"> + <source>Your connected wallet raised an unknown error. Message: %s</source> + <target state="new">Your connected wallet raised an unknown error. Message: %s</target> + <note>Human readable error description for unknown error</note> + </trans-unit> <trans-unit id="Your content is being broadcasted to the network. Please wait." xml:space="preserve"> <source>Your content is being broadcasted to the network. Please wait.</source> <target state="new">Your content is being broadcasted to the network. Please wait.</target> @@ -7273,6 +7530,21 @@ User confirm Yes</note> <target state="new">Your report will be sent to the relays you are connected to</target> <note>Footer text to inform user what will happen when the report is submitted.</note> </trans-unit> + <trans-unit id="Your transaction quota has been exceeded." xml:space="preserve"> + <source>Your transaction quota has been exceeded.</source> + <target state="new">Your transaction quota has been exceeded.</target> + <note>Error description for quota exceeded</note> + </trans-unit> + <trans-unit id="Your wallet does not have sufficient balance for this transaction." xml:space="preserve"> + <source>Your wallet does not have sufficient balance for this transaction.</source> + <target state="new">Your wallet does not have sufficient balance for this transaction.</target> + <note>Error description for insufficient balance</note> + </trans-unit> + <trans-unit id="Your wallet is temporarily being rate limited." xml:space="preserve"> + <source>Your wallet is temporarily being rate limited.</source> + <target state="new">Your wallet is temporarily being rate limited.</target> + <note>Error description for rate limit error</note> + </trans-unit> <trans-unit id="Zap" xml:space="preserve"> <source>Zap</source> <target state="new">Zap</target> diff --git a/damus/en-US.xcloc/Source Contents/damus/Localizable.xcstrings b/damus/en-US.xcloc/Source Contents/damus/Localizable.xcstrings @@ -177,6 +177,9 @@ "An additional percentage of each zap will be sent to support Damus development" : { "comment" : "Text indicating that they can contribute zaps to support Damus development." }, + "An internal error occurred in your wallet." : { + "comment" : "Error description for an internal error" + }, "An unexpected error happened while trying to perform this action. Please contact support." : { "comment" : "Error message for a failed reset/repair operation" }, @@ -186,6 +189,9 @@ "An unknown error occurred while adding a relay." : { "comment" : "Title of an unknown relay error message." }, + "An unspecified error occurred in your wallet." : { + "comment" : "Error description for an unspecified error" + }, "Animations" : { "comment" : "Toggle to enable or disable image animation" }, @@ -303,6 +309,9 @@ "Check the address and/or the relay list." : { "comment" : "Human readable tip for error" }, + "Check your account permissions or contact support." : { + "comment" : "Tip for restricted operation" + }, "Check your internet connection and try again. If the error persists, contact support." : { "comment" : "Error tip when user tries to create the one-click Coinos wallet setup but fails for a generic reason." }, @@ -330,7 +339,7 @@ "Close" : { "comment" : "Button label giving the user the option to close the sheet due to not being logged in.\nButton label giving the user the option to close the sheet from which they shared content\nButton label giving the user the option to close the sheet from which they were trying share.\nButton label giving the user the option to close the sheet from which they were trying to share.\nButton label giving the user the option to close the view when no content is available to share" }, - "Coinos is a service operated by a third-party. We have no access to your Coinos wallet." : { + "Coinos is a service operated by a third-party. The Damus team has no access to your wallet." : { "comment" : "Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service." }, "Coming soon" : { @@ -387,6 +396,9 @@ "Copied" : { "comment" : "Label indicating that a user's key was copied." }, + "Copied!" : { + "comment" : "Label indicating that the error technical information was successfully copied to the clipboard, which shows up as soon as the user clicks the copy button." + }, "Copy" : { "comment" : "Button to copy a relay server address.\nButton to copy the value found.\nContext menu option for copying the version of damus." }, @@ -417,6 +429,9 @@ "Copy Report ID" : { "comment" : "Button to copy report ID." }, + "Copy technical information" : { + "comment" : "Button label to allow user to copy technical information from an error screen (usually to provide our support team for further troubleshooting)" + }, "Copy text" : { "comment" : "Context menu option for copying the text from an note." }, @@ -501,6 +516,9 @@ "Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode." : { "comment" : "Section header for Developer Settings view" }, + "Disable high balance warning" : { + "comment" : "Setting to disable high balance warnings on the user's wallet" + }, "Discard changes?" : { "comment" : "Alert user that changes have been made." }, @@ -511,7 +529,7 @@ "comment" : "Text for button to disconnect from Nostr Wallet Connect lightning wallet." }, "Dismiss" : { - "comment" : "Button to dismiss alert\nButton to dismiss error" + "comment" : "Button label to dismiss the safety reminder that the user's wallet has a high balance\nButton to dismiss alert\nButton to dismiss error" }, "DMs" : { "comment" : "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.\nNavigation title for view of DMs, where DM is an English abbreviation for Direct Message.\nPicker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.\nSetting to enable DM Local Notification\nToolbar label for DMs view, where DM is the English abbreviation for Direct Message." @@ -540,6 +558,9 @@ "Edit profile picture" : { "comment" : "Accessibility label for a button that edits a profile picture" }, + "Empty error message" : { + "comment" : "A human readable placeholder to indicate that the error message is empty" + }, "Enable experimental Purple API support" : { "comment" : "Developer mode setting to enable experimental Purple API support." }, @@ -1099,6 +1120,9 @@ "Nudity" : { "comment" : "Description of report type for nudity." }, + "NWC wallet" : { + "comment" : "Title for section in zap settings that controls general NWC wallet settings." + }, "Ok" : { "comment" : "Button to dismiss the alert." }, @@ -1171,6 +1195,9 @@ "Plan" : { "comment" : "Prompt selection of DeepL subscription plan to perform machine translations on notes" }, + "Please check for updates or contact your wallet provider." : { + "comment" : "Tip for not implemented error" + }, "Please check the address and try again" : { "comment" : "Tip for an error where the relay address being added is invalid" }, @@ -1183,9 +1210,18 @@ "Please contact support." : { "comment" : "Tip for an unknown relay error message." }, + "Please contact the developer of your wallet provider for help." : { + "comment" : "Human readable error description for an unknown error raised by a wallet provider." + }, "Please contact the person who provided the link, and ask for another link." : { "comment" : "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference." }, + "Please copy the technical info and send it to our support team." : { + "comment" : "Tip on how to resolve issue when wallet returns an invalid response" + }, + "Please deposit more funds and try again." : { + "comment" : "Tip for insufficient balance errors" + }, "Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." : { "comment" : "User-facing tips on what to do if a Purple welcome link doesn't work" }, @@ -1198,12 +1234,18 @@ "Please try again later or contact support if the issue persists." : { "comment" : "Human readable tip for error" }, + "Please try again or contact your wallet provider for further assistance." : { + "comment" : "Tip for unspecified error" + }, "Please try again, check the URL for typos, or contact support for further help." : { "comment" : "User visible error tips" }, "Please try opening this content on another Nostr app that supports this type of content." : { "comment" : "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing." }, + "Please verify your credentials or permissions." : { + "comment" : "Tip for unauthorized access" + }, "Point your camera to a QR code…" : { "comment" : "Text on QR code camera view instructing user to point to QR code" }, @@ -1701,6 +1743,9 @@ "This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io." : { "comment" : "Notice label that user cannot manage their In-App purchases" }, + "This feature is not implemented by your wallet." : { + "comment" : "Error description for not implemented feature" + }, "This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective." : { "comment" : "Warning that the inputted account key is a public key and the result of what happens because of it." }, @@ -1713,6 +1758,9 @@ "This note contains too many items and cannot be rendered" : { "comment" : "Error message indicating that a note is too big and cannot be rendered" }, + "This operation is restricted by your wallet." : { + "comment" : "Error description for restricted operation" + }, "This relay is already in your list." : { "comment" : "Human readable tip for error" }, @@ -1761,6 +1809,9 @@ "Try checking the link again, your internet connection, or contact the person who provided you the link for help." : { "comment" : "Tips on what to do if a note cannot be found." }, + "Try restarting your wallet or contacting support if the problem persists." : { + "comment" : "Tip for internal error" + }, "Type %@ to delete" : { "comment" : "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account." }, @@ -1854,9 +1905,24 @@ "Visit the Damus website on a web browser to manage billing" : { "comment" : "Instruction on how to manage billing externally" }, + "Wait a few moments, and then try again." : { + "comment" : "Tip for rate limit error" + }, + "Wait for the quota to reset, or configure your wallet provider to allow a higher limit." : { + "comment" : "Tip for quota exceeded" + }, "Wallet" : { "comment" : "Navigation title for Wallet view\nNavigation title for attaching Nostr Wallet Connect lightning wallet.\nSidebar menu label for Wallet view.\nTitle for section in zap settings that controls the Lightning wallet selection." }, + "Wallet provider returned a response that we could not decrypt." : { + "comment" : "Error description shown to the user when a response from the wallet provider contains data the app could not decrypt." + }, + "Wallet provider returned a response that we do not understand." : { + "comment" : "Error description shown to the user when a response from the wallet provider contains data the app does not understand" + }, + "Wallet provider returned an invalid response." : { + "comment" : "Error description shown to the user when a response from the wallet provider is invalid" + }, "WARNING:\n\nThis will attempt to repair your relay list based on other information we have. You may lose any relays you have added manually. Only proceed if you have lost your relay list beyond recoverability or if you are ok with losing any manually added relays." : { "comment" : "Alert for repairing the user's relay list." }, @@ -1926,6 +1992,9 @@ "you" : { "comment" : "You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself." }, + "You are not authorized to perform this action with your wallet." : { + "comment" : "Error description for unauthorized access" + }, "You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : { "comment" : "Label explaining that sharing cannot proceed because the user is not logged in." }, @@ -1953,6 +2022,9 @@ "You unlocked" : { "comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple" }, + "Your connected wallet raised an unknown error. Message: %s" : { + "comment" : "Human readable error description for unknown error" + }, "Your content is being broadcasted to the network. Please wait." : { "comment" : "Label explaining that their content sharing action is in progress" }, @@ -1980,6 +2052,15 @@ "Your report will be sent to the relays you are connected to" : { "comment" : "Footer text to inform user what will happen when the report is submitted." }, + "Your transaction quota has been exceeded." : { + "comment" : "Error description for quota exceeded" + }, + "Your wallet does not have sufficient balance for this transaction." : { + "comment" : "Error description for insufficient balance" + }, + "Your wallet is temporarily being rate limited." : { + "comment" : "Error description for rate limit error" + }, "Zap" : { "comment" : "Accessibility label for zap button\nButton label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen\nText underneath the number of sats indicating that it's the amount used for zaps.\nTitle of notification when a non-private zap is received." }, diff --git a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings Binary files differ. diff --git a/damus/nl.lproj/Localizable.strings b/damus/nl.lproj/Localizable.strings Binary files differ. diff --git a/damus/th.lproj/Localizable.strings b/damus/th.lproj/Localizable.strings Binary files differ. diff --git a/damusTests/Benchmarking.swift b/damusTests/Benchmarking.swift @@ -0,0 +1,72 @@ +// +// Benchmarking.swift +// damusTests +// +// Created by William Casarin on 3/6/25. +// + +import Testing +import XCTest +@testable import damus + +class BenchmarkingTests: XCTestCase { + + // Old regex-based implementations for comparison + func trim_suffix_regex(_ str: String) -> String { + return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) + } + + func trim_prefix_regex(_ str: String) -> String { + return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression) + } + + // Test strings with different characteristics + lazy var testStrings: [String] = [ + " Hello World ", // Simple whitespace + " \n\t Hello World \n\t ", // Mixed whitespace + String(repeating: " ", count: 1000) + "Hello", // Large prefix + "Hello" + String(repeating: " ", count: 1000), // Large suffix + String(repeating: " ", count: 500) + "Hello" + String(repeating: " ", count: 500) // Both + ] + + func testTrimSuffixRegexPerformance() throws { + measure { + for str in testStrings { + _ = trim_suffix_regex(str) + } + } + } + + func testTrimSuffixNewPerformance() throws { + measure { + for str in testStrings { + _ = trim_suffix(str) + } + } + } + + func testTrimPrefixRegexPerformance() throws { + measure { + for str in testStrings { + _ = trim_prefix_regex(str) + } + } + } + + func testTrimPrefixNewPerformance() throws { + measure { + for str in testStrings { + _ = trim_prefix(str) + } + } + } + + func testTrimFunctionCorrectness() throws { + // Verify that both implementations produce the same results + for str in testStrings { + XCTAssertEqual(trim_suffix(str), trim_suffix_regex(str), "New trim_suffix implementation produces different results") + XCTAssertEqual(trim_prefix(str), trim_prefix_regex(str), "New trim_prefix implementation produces different results") + } + } +} + diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift @@ -49,7 +49,8 @@ func generate_test_damus_state( video: .init(), ndb: ndb, quote_reposts: .init(our_pubkey: our_pubkey), - emoji_provider: DefaultEmojiProvider(showAllVariations: false) + emoji_provider: DefaultEmojiProvider(showAllVariations: false), + favicon_cache: .init() ) return damus diff --git a/damusTests/NIP05DomainTimelineHeaderViewTests.swift b/damusTests/NIP05DomainTimelineHeaderViewTests.swift @@ -0,0 +1,39 @@ +// +// NIP05DomainTimelineHeaderViewTests.swift +// damusTests +// +// Created by Terry Yiu on 5/23/25. +// + +import XCTest +@testable import damus + +final class NIP05DomainTimelineHeaderViewTests: XCTestCase { + + let enUsLocale = Locale(identifier: "en-US") + + func testFriendsOfFriendsString() throws { + let pk1 = test_pubkey + let pk2 = test_pubkey_2 + let pk3 = Pubkey(hex: "b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0")! + let pk4 = Pubkey(hex: "cc590e46363d0fa66bb27081368d01f169b8ffc7c614629d4e9eef6c88b38670")! + let pk5 = Pubkey(hex: "f2aa579bb998627e04a8f553842a09446360c9d708c6141dd119c479f6ab9d29")! + + let ndb = Ndb(path: Ndb.db_path)! + + let damus_name = "17ldvg64:nq5mhr77" + XCTAssertEqual(friendsOfFriendsString([pk1], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name)") + XCTAssertEqual(friendsOfFriendsString([pk1, pk2], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name) & 1rppft3m:4qxhsgnj") + XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj & 1kshyfd2:cq04aze0") + XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4,], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 1 other in your trusted network") + XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4, pk5], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 2 others in your trusted network") + + let pubkeys = [pk1, pk2, pk3, pk4, pk5, pk1, pk2, pk3, pk4, pk5] + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(friendsOfFriendsString(pubkeys.prefix(count).map { $0 }, ndb: ndb, locale: $0)) + } + } + } + +}