diff --git a/CHANGELOG.md b/CHANGELOG.md index 776fbd4e..41c0faf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ + +## [1.1.0-9] - 2023-02-26 + +### Added + +- Customized zaps (William Casarin) +- Add new Notifications View (William Casarin) +- Bookmarking (Joel Klabo) + +### Changed + +- No more inline npubs when tagging users (Swift) + + +### Fixed + +- Fix alignment of side menu labels (Joel Klabo) +- Fix duplicated participants in reply-to view (Joel Klabo) +- Load missing profiles in Zaps view (William Casarin) +- Fix memory leak with inline videos (William Casarin) +- Eliminate popping when scrolling (William Casarin) + + +[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9 + ## [1.1.0-3] - 2023-02-20 ### Added diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 94884168..4423a98a 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -11,6 +11,11 @@ 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; }; 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; + 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; }; + 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; }; + 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; }; + 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; }; + 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; @@ -43,6 +48,11 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; }; 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; }; 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; }; + 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; }; + 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; }; + 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; }; + 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; }; + 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */; }; 4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; }; 4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; }; 4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; }; @@ -93,6 +103,9 @@ 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; }; 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; }; 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; }; + 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; }; + 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; }; + 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; }; 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; }; 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; }; 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; }; @@ -122,6 +135,8 @@ 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; }; + 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; + 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; }; 4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; }; @@ -216,6 +231,7 @@ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; }; 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; }; 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; + 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; @@ -258,6 +274,18 @@ 3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = ""; }; 3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = ""; }; + 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = ""; }; + 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = ""; }; + 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = ""; }; + 3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = ""; }; + 3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; + 3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = ""; }; + 3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; + 3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = ""; }; + 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = ""; }; 3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = ""; }; 3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; 3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -331,6 +359,11 @@ 4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = ""; }; 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = ""; }; 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; + 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = ""; }; + 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = ""; }; + 4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = ""; }; + 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.swift; sourceTree = ""; }; 4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; 4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = ""; }; 4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; @@ -411,6 +444,9 @@ 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = ""; }; 4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = ""; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = ""; }; + 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = ""; }; + 4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = ""; }; + 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = ""; }; 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = ""; }; 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = ""; }; @@ -440,6 +476,8 @@ 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = ""; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = ""; }; 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = ""; }; + 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = ""; }; + 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = ""; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = ""; }; 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = ""; }; @@ -536,6 +574,7 @@ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = ""; }; 7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = ""; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = ""; }; + 9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = ""; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = ""; }; @@ -651,6 +690,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + 4C54AA0829A55416003E4487 /* Notifications */, 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, @@ -690,13 +730,35 @@ 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */, 4CE8795A2996C47A00F758CC /* ZapsModel.swift */, 3AA59D1C2999B0400061C48E /* DraftsModel.swift */, + 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */, ); path = Models; sourceTree = ""; }; + 4C30AC7029A5676F00E2BD5A /* Notifications */ = { + isa = PBXGroup; + children = ( + 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */, + 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */, + 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */, + 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */, + ); + path = Notifications; + sourceTree = ""; + }; + 4C54AA0829A55416003E4487 /* Notifications */ = { + isa = PBXGroup; + children = ( + 4C54AA0929A55429003E4487 /* EventGroup.swift */, + 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */, + ); + path = Notifications; + sourceTree = ""; + }; 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4C30AC7029A5676F00E2BD5A /* Notifications */, 4CE0E2B029A3DF4700DB4CA2 /* Timeline */, 4CE879562996C44A00F758CC /* Zaps */, 4CB9D4A52992D01900A9A7E4 /* Profile */, @@ -729,6 +791,7 @@ 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */, + 9C83F89229A937B900136C08 /* TextViewWrapper.swift */, 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, @@ -812,6 +875,8 @@ 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */, 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */, 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */, + 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */, + 4C30AC7729A577AB00E2BD5A /* EventCache.swift */, ); path = Util; sourceTree = ""; @@ -853,6 +918,7 @@ children = ( 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */, 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */, + 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */, ); path = Profile; sourceTree = ""; @@ -974,6 +1040,10 @@ 4CF0ABDB2981A19E00D66079 /* ListTests.swift */, 4CB883A9297612FF00DC99E7 /* ZapTests.swift */, 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */, + 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */, + 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */, + 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */, + 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, ); path = damusTests; sourceTree = ""; @@ -1008,6 +1078,7 @@ isa = PBXGroup; children = ( 4CE879572996C45300F758CC /* ZapsView.swift */, + 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, ); path = Zaps; sourceTree = ""; @@ -1163,6 +1234,8 @@ id, cs, ru, + "zh-HK", + "zh-TW", ); mainGroup = 4CE6DEDA27F7A08100C66700; packageReferences = ( @@ -1219,6 +1292,7 @@ 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 4C363A8A28236B57006E126D /* MentionView.swift in Sources */, 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, + 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, @@ -1231,6 +1305,7 @@ 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, + 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */, 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */, @@ -1242,6 +1317,7 @@ 4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, + 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */, 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, @@ -1273,6 +1349,7 @@ 4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, + 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */, 4C363A8428233689006E126D /* Parser.swift in Sources */, 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */, 4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */, @@ -1284,6 +1361,7 @@ E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */, + 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */, 4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, @@ -1292,6 +1370,7 @@ 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */, F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */, 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */, + 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */, F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */, 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */, 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */, @@ -1321,16 +1400,19 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4CE879582996C45300F758CC /* ZapsView.swift in Sources */, + 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */, 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */, 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, + 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, + 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, @@ -1351,6 +1433,7 @@ 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, + 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */, @@ -1380,6 +1463,7 @@ 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, + 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, @@ -1404,6 +1488,7 @@ 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */, 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, + 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, @@ -1419,8 +1504,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, + 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */, + 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */, 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, 5023E76329AA3627007D3D50 /* RelayPoolTests.swift in Sources */, 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */, @@ -1431,6 +1519,7 @@ 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */, 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */, 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */, + 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */, 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1480,6 +1569,8 @@ 3A41E55B299D52BE001FA465 /* id */, 3A8624DB299E82BE00BD8BE9 /* cs */, 3A827A1A299FC69D00C4D171 /* ru */, + 3A3040FB29A91F03008A0F29 /* zh-HK */, + 3A3040FD29A91F31008A0F29 /* zh-TW */, ); name = Localizable.stringsdict; sourceTree = ""; @@ -1503,6 +1594,8 @@ 3A41E559299D52BE001FA465 /* id */, 3A8624D9299E82BE00BD8BE9 /* cs */, 3A827A18299FC69D00C4D171 /* ru */, + 3A3040F929A91ED6008A0F29 /* zh-HK */, + 3A3040FC29A91F31008A0F29 /* zh-TW */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1526,6 +1619,9 @@ 3A41E55A299D52BE001FA465 /* id */, 3A8624DA299E82BE00BD8BE9 /* cs */, 3A827A19299FC69D00C4D171 /* ru */, + 3A3040FA29A91EFC008A0F29 /* zh-HK */, + 3A3040FE29A91F31008A0F29 /* zh-TW */, + 3A3040FF29AB02D1008A0F29 /* en-US */, ); name = Localizable.strings; sourceTree = ""; @@ -1661,7 +1757,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; @@ -1703,7 +1799,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift index 19540663..09089c31 100644 --- a/damus/Components/ImageCarousel.swift +++ b/damus/Components/ImageCarousel.swift @@ -205,6 +205,7 @@ struct ImageCarousel: View { view.framePreloadCount = 3 } .aspectRatio(contentMode: .fit) + .cornerRadius(10) .tabItem { Text(url.absoluteString) } @@ -217,11 +218,11 @@ struct ImageCarousel: View { } } } - .cornerRadius(10) .fullScreenCover(isPresented: $open_sheet) { ImageView(urls: urls) } .frame(height: 200) + .clipped() .onTapGesture { open_sheet = true } diff --git a/damus/Components/UserView.swift b/damus/Components/UserView.swift index 721a5f40..d51aca67 100644 --- a/damus/Components/UserView.swift +++ b/damus/Components/UserView.swift @@ -12,11 +12,7 @@ struct UserView: View { let pubkey: String var body: some View { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let followers = FollowersModel(damus_state: damus_state, target: pubkey) - let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers) - - NavigationLink(destination: pv) { + NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) { ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) VStack(alignment: .leading) { diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift index 36c3ea23..fff6f902 100644 --- a/damus/Components/ZapButton.swift +++ b/damus/Components/ZapButton.swift @@ -7,6 +7,22 @@ import SwiftUI +enum ZappingEventType { + case failed(ZappingError) + case got_zap_invoice(String) +} + +enum ZappingError { + case fetching_invoice + case bad_lnurl +} + +struct ZappingEvent { + let is_custom: Bool + let type: ZappingEventType + let event: NostrEvent +} + struct ZapButton: View { let damus_state: DamusState let event: NostrEvent @@ -19,61 +35,8 @@ struct ZapButton: View { @State var slider_value: Double = 0.0 @State var slider_visible: Bool = false @State var showing_select_wallet: Bool = false - - func send_zap() { - guard let privkey = damus_state.keypair.privkey else { - return - } - - // Only take the first 10 because reasons - let relays = Array(damus_state.pool.descriptors.prefix(10)) - let target = ZapTarget.note(id: event.id, author: event.pubkey) - // TODO: gather comment? - let content = "" - let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target) - - zapping = true - - Task { - var mpayreq = damus_state.lnurls.lookup(target.pubkey) - if mpayreq == nil { - mpayreq = await fetch_static_payreq(lnurl) - } - - guard let payreq = mpayreq else { - // TODO: show error - DispatchQueue.main.async { - zapping = false - } - return - } - - DispatchQueue.main.async { - damus_state.lnurls.endpoints[target.pubkey] = payreq - } - - let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000 - guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else { - DispatchQueue.main.async { - zapping = false - } - return - } - - DispatchQueue.main.async { - zapping = false - - if should_show_wallet_selector(damus_state.pubkey) { - self.invoice = inv - self.showing_select_wallet = true - } else { - open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv) - } - } - } - - //damus_state.pool.send(.event(zapreq)) - } + @State var showing_zap_customizer: Bool = false + @State var is_charging: Bool = false var zap_img: String { if bar.zapped { @@ -92,6 +55,10 @@ struct ZapButton: View { return Color.orange } + if is_charging { + return Color.yellow + } + if !zapping { return nil } @@ -101,22 +68,62 @@ struct ZapButton: View { var body: some View { HStack(spacing: 4) { - EventActionButton(img: zap_img, col: zap_color) { - if bar.zapped { - //notify(.delete, bar.our_tip) - } else if !zapping { - send_zap() + Image(systemName: zap_img) + .foregroundColor(zap_color == nil ? Color.gray : zap_color!) + .font(.footnote.weight(.medium)) + .onTapGesture { + if bar.zapped { + //notify(.delete, bar.our_tip) + } else if !zapping { + self.showing_zap_customizer = true + //send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false) + //self.zapping = true + } } + .onLongPressGesture(minimumDuration: 0, pressing: { is_charing in + self.is_charging = is_charging + }, perform: { + self.showing_zap_customizer = true + }) + .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) + + if bar.zap_total > 0 { + Text(verbatim: format_msats_abbrev(bar.zap_total)) + .font(.footnote) + .foregroundColor(bar.zapped ? Color.orange : Color.gray) } - .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) - - Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")) - .font(.footnote) - .foregroundColor(bar.zapped ? Color.orange : Color.gray) + } + .sheet(isPresented: $showing_zap_customizer) { + CustomizeZapView(state: damus_state, event: event, lnurl: lnurl) } .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice) } + .onReceive(handle_notify(.zapping)) { notif in + let zap_ev = notif.object as! ZappingEvent + + guard zap_ev.event.id == self.event.id else { + return + } + + guard !zap_ev.is_custom else { + return + } + + switch zap_ev.type { + case .failed: + break + case .got_zap_invoice(let inv): + if should_show_wallet_selector(damus_state.pubkey) { + self.invoice = inv + self.showing_select_wallet = true + } else { + open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv) + } + } + + self.zapping = false + } } } @@ -128,3 +135,55 @@ struct ZapButton_Previews: PreviewProvider { } } + + +func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { + guard let privkey = damus_state.keypair.privkey else { + return + } + + // Only take the first 10 because reasons + let relays = Array(damus_state.pool.descriptors.prefix(10)) + let target = ZapTarget.note(id: event.id, author: event.pubkey) + let content = comment ?? "" + let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target, is_anon: zap_type == .anon) + + Task { + var mpayreq = damus_state.lnurls.lookup(target.pubkey) + if mpayreq == nil { + mpayreq = await fetch_static_payreq(lnurl) + } + + guard let payreq = mpayreq else { + // TODO: show error + DispatchQueue.main.async { + let typ = ZappingEventType.failed(.bad_lnurl) + let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) + notify(.zapping, ev) + } + return + } + + DispatchQueue.main.async { + damus_state.lnurls.endpoints[target.pubkey] = payreq + } + + let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000 + + guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else { + DispatchQueue.main.async { + let typ = ZappingEventType.failed(.fetching_invoice) + let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) + notify(.zapping, ev) + } + return + } + + DispatchQueue.main.async { + let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event) + notify(.zapping, ev) + } + } + + return +} diff --git a/damus/ContentView.swift b/damus/ContentView.swift index f3416eae..6edfadd6 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -166,7 +166,7 @@ struct ContentView: View { Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.") .bold() case .none: - Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.") + Text(verbatim: "") } } } @@ -192,7 +192,7 @@ struct ContentView: View { case .notifications: VStack(spacing: 0) { Divider() - TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true }) + NotificationsView(state: damus, notifications: home.notifications) } case .dms: DirectMessagesView(damus_state: damus_state!) @@ -615,7 +615,8 @@ struct ContentView: View { settings: UserSettingsStore(), relay_filters: relay_filters, relay_metadata: metadatas, - drafts: Drafts() + drafts: Drafts(), + events: EventCache() ) home.damus_state = self.damus_state! diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index 5739f5bc..debc3010 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -24,6 +24,7 @@ struct DamusState { let relay_filters: RelayFilters let relay_metadata: RelayMetadatas let drafts: Drafts + let events: EventCache var pubkey: String { return keypair.pubkey @@ -32,9 +33,8 @@ struct DamusState { var is_privkey_user: Bool { keypair.privkey != nil } - static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts()) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache()) } } diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift index b9912835..8e36ed1c 100644 --- a/damus/Models/DraftsModel.swift +++ b/damus/Models/DraftsModel.swift @@ -8,6 +8,6 @@ import Foundation class Drafts: ObservableObject { - @Published var post: String = "" - @Published var replies: [NostrEvent: String] = [:] + @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "") + @Published var replies: [NostrEvent: NSMutableAttributedString] = [:] } diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index cb110226..1a8de5c5 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -65,7 +65,7 @@ class EventsModel: ObservableObject { case .notice(_): break case .eose(_): - load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state) + load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state) } } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index e9527211..fd502ec1 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -50,22 +50,18 @@ class HomeModel: ObservableObject { let profiles_subid = UUID().description @Published var new_events: NewEventsBits = NewEventsBits() - @Published var notifications: EventHolder + @Published var notifications = NotificationsModel() @Published var dms: DirectMessagesModel - @Published var events: EventHolder + @Published var events = EventHolder() @Published var loading: Bool = false @Published var signal: SignalModel = SignalModel() init() { - self.events = EventHolder() - self.notifications = EventHolder() self.damus_state = DamusState.empty self.dms = DirectMessagesModel(our_pubkey: "") } init(damus_state: DamusState) { - self.events = EventHolder() - self.notifications = EventHolder() self.damus_state = damus_state self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.setup_debouncer() @@ -129,6 +125,8 @@ class HomeModel: ObservableObject { handle_channel_meta(ev) case .zap: handle_zap_event(ev) + case .zap_request: + break } } @@ -143,7 +141,7 @@ class HomeModel: ObservableObject { return } - if !notifications.insert(ev) { + if !notifications.insert_zap(zap) { return } @@ -229,7 +227,7 @@ class HomeModel: ObservableObject { guard inner_ev.is_valid else { return } - + if inner_ev.is_textlike { handle_text_event(sub_id: sub_id, ev) } @@ -255,12 +253,11 @@ class HomeModel: ObservableObject { return } - // CHECK SIGS ON THESE - switch damus_state.likes.add_event(ev, target: e.ref_id) { case .already_counted: break case .success(let n): + handle_notification(ev: ev) let liked = Counted(event: ev, id: e.ref_id, total: n) notify(.liked, liked) notify(.update_stats, e.ref_id) @@ -320,9 +317,9 @@ class HomeModel: ObservableObject { if sub_id == dms_subid { var dms = dms.dms.flatMap { $0.1.events } dms.append(contentsOf: incoming_dms) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state) } else if sub_id == notifications_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state) } self.loading = false @@ -375,7 +372,6 @@ class HomeModel: ObservableObject { // TODO: separate likes? var home_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, - NostrKind.chat.rawValue, NostrKind.like.rawValue, NostrKind.boost.rawValue, ]) @@ -385,7 +381,6 @@ class HomeModel: ObservableObject { var notifications_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, - NostrKind.chat.rawValue, NostrKind.like.rawValue, NostrKind.boost.rawValue, NostrKind.zap.rawValue, @@ -461,7 +456,16 @@ class HomeModel: ObservableObject { return } - if !notifications.insert(ev) { + guard should_show_event(contacts: damus_state.contacts, ev: ev) else { + return + } + + damus_state.events.insert(ev) + if let inner_ev = ev.inner_event { + damus_state.events.insert(inner_ev) + } + + if !notifications.insert_event(ev) { return } @@ -484,6 +488,8 @@ class HomeModel: ObservableObject { guard should_show_event(contacts: damus_state.contacts, ev: ev) else { return } + + damus_state.events.insert(ev) if sub_id == home_subid { insert_home_event(ev) diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift index 445bb707..fb408840 100644 --- a/damus/Models/Mentions.swift +++ b/damus/Models/Mentions.swift @@ -263,17 +263,19 @@ func format_msats_abbrev(_ msats: Int64) -> String { return formatter.string(from: sats) ?? sats.stringValue } -func format_msats(_ msat: Int64) -> String { +func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 0 numberFormatter.maximumFractionDigits = 3 numberFormatter.roundingMode = .down + numberFormatter.locale = locale let sats = NSNumber(value: (Double(msat) / 1000.0)) let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue - return String(format: Bundle.main.localizedString(forKey: "sats_count", value: nil, table: nil), sats.decimalValue as NSDecimalNumber, formattedSats) + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "sats_count", value: nil, table: nil), locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats) } func convert_invoice_block(_ b: invoice_block) -> Block? { diff --git a/damus/Models/Notifications/EventGroup.swift b/damus/Models/Notifications/EventGroup.swift new file mode 100644 index 00000000..5d4fe852 --- /dev/null +++ b/damus/Models/Notifications/EventGroup.swift @@ -0,0 +1,32 @@ +// +// ReactionGroup.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class EventGroup { + var events: [NostrEvent] + + var last_event_at: Int64 { + guard let first = self.events.first else { + return 0 + } + + return first.created_at + } + + init() { + self.events = [] + } + + init(events: [NostrEvent]) { + self.events = events + } + + func insert(_ ev: NostrEvent) -> Bool { + return insert_uniq_sorted_event_created(events: &events, new_ev: ev) + } +} diff --git a/damus/Models/Notifications/ZapGroup.swift b/damus/Models/Notifications/ZapGroup.swift new file mode 100644 index 00000000..001367db --- /dev/null +++ b/damus/Models/Notifications/ZapGroup.swift @@ -0,0 +1,53 @@ +// +// ZapGroup.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class ZapGroup { + var zaps: [Zap] + var msat_total: Int64 + var zappers: Set + + var last_event_at: Int64 { + guard let first = zaps.first else { + return 0 + } + + return first.event.created_at + } + + func zap_requests() -> [NostrEvent] { + zaps.map { z in z.request.ev } + } + + init(zaps: [Zap]) { + self.zaps = zaps + self.msat_total = 0 + self.zappers = Set() + } + + init() { + self.zaps = [] + self.msat_total = 0 + self.zappers = Set() + } + + func insert(_ zap: Zap) -> Bool { + if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) { + return false + } + + msat_total += zap.invoice.amount + + if !zappers.contains(zap.request.ev.pubkey) { + zappers.insert(zap.request.ev.pubkey) + } + + return true + } +} + diff --git a/damus/Models/NotificationsModel.swift b/damus/Models/NotificationsModel.swift new file mode 100644 index 00000000..82826e68 --- /dev/null +++ b/damus/Models/NotificationsModel.swift @@ -0,0 +1,298 @@ +// +// NotificationsModel.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +enum NotificationItem { + case repost(String, EventGroup) + case reaction(String, EventGroup) + case profile_zap(ZapGroup) + case event_zap(String, ZapGroup) + case reply(NostrEvent) + + var id: String { + switch self { + case .repost(let evid, _): + return "repost_" + evid + case .reaction(let evid, _): + return "reaction_" + evid + case .profile_zap: + return "profile_zap" + case .event_zap(let evid, _): + return "event_zap_" + evid + case .reply(let ev): + return "reply_" + ev.id + } + } + + var last_event_at: Int64 { + switch self { + case .reaction(_, let evgrp): + return evgrp.last_event_at + case .repost(_, let evgrp): + return evgrp.last_event_at + case .profile_zap(let zapgrp): + return zapgrp.last_event_at + case .event_zap(_, let zapgrp): + return zapgrp.last_event_at + case .reply(let reply): + return reply.created_at + } + } +} + +class NotificationsModel: ObservableObject, ScrollQueue { + var incoming_zaps: [Zap] + var incoming_events: [NostrEvent] + var should_queue: Bool + + // mappings from events to + var zaps: [String: ZapGroup] + var profile_zaps: ZapGroup + var reactions: [String: EventGroup] + var reposts: [String: EventGroup] + var replies: [NostrEvent] + var has_reply: Set + + @Published var notifications: [NotificationItem] + + init() { + self.zaps = [:] + self.reactions = [:] + self.reposts = [:] + self.replies = [] + self.has_reply = Set() + self.should_queue = true + self.incoming_zaps = [] + self.incoming_events = [] + self.profile_zaps = ZapGroup() + self.notifications = [] + } + + func set_should_queue(_ val: Bool) { + self.should_queue = val + } + + func uniq_pubkeys() -> [String] { + var pks = Set() + + for ev in incoming_events { + pks.insert(ev.pubkey) + } + + for grp in reposts { + for ev in grp.value.events { + pks.insert(ev.pubkey) + } + } + + for ev in replies { + pks.insert(ev.pubkey) + } + + for zap in incoming_zaps { + pks.insert(zap.request.ev.pubkey) + } + + return Array(pks) + } + + func build_notifications() -> [NotificationItem] { + var notifs: [NotificationItem] = [] + + for el in zaps { + let evid = el.key + let zapgrp = el.value + + let notif: NotificationItem = .event_zap(evid, zapgrp) + notifs.append(notif) + } + + if !profile_zaps.zaps.isEmpty { + notifs.append(.profile_zap(profile_zaps)) + } + + for el in reposts { + let evid = el.key + let evgrp = el.value + + notifs.append(.repost(evid, evgrp)) + } + + for el in reactions { + let evid = el.key + let evgrp = el.value + + notifs.append(.reaction(evid, evgrp)) + } + + for reply in replies { + notifs.append(.reply(reply)) + } + + notifs.sort { $0.last_event_at > $1.last_event_at } + return notifs + } + + + private func insert_repost(_ ev: NostrEvent) -> Bool { + guard let reposted_ev = ev.inner_event else { + return false + } + + let id = reposted_ev.id + + if let evgrp = self.reposts[id] { + return evgrp.insert(ev) + } else { + let evgrp = EventGroup() + self.reposts[id] = evgrp + return evgrp.insert(ev) + } + } + + private func insert_text(_ ev: NostrEvent) -> Bool { + guard !has_reply.contains(ev.id) else { + return false + } + + has_reply.insert(ev.id) + replies.append(ev) + + return true + } + + private func insert_reaction(_ ev: NostrEvent) -> Bool { + guard let ref_id = ev.referenced_ids.last else { + return false + } + + let id = ref_id.id + + if let evgrp = self.reactions[id] { + return evgrp.insert(ev) + } else { + let evgrp = EventGroup() + self.reactions[id] = evgrp + return evgrp.insert(ev) + } + } + + private func insert_event_immediate(_ ev: NostrEvent) -> Bool { + if ev.known_kind == .boost { + return insert_repost(ev) + } else if ev.known_kind == .like { + return insert_reaction(ev) + } else if ev.known_kind == .text { + return insert_text(ev) + } + + return false + } + + private func insert_zap_immediate(_ zap: Zap) -> Bool { + switch zap.target { + case .note(let notezt): + let id = notezt.note_id + if let zapgrp = self.zaps[notezt.note_id] { + return zapgrp.insert(zap) + } else { + let zapgrp = ZapGroup() + self.zaps[id] = zapgrp + return zapgrp.insert(zap) + } + + case .profile: + return profile_zaps.insert(zap) + } + } + + func insert_event(_ ev: NostrEvent) -> Bool { + if should_queue { + return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev) + } + + if insert_event_immediate(ev) { + self.notifications = build_notifications() + return true + } + + return false + } + + func insert_zap(_ zap: Zap) -> Bool { + if should_queue { + return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap) + } + + if insert_zap_immediate(zap) { + self.notifications = build_notifications() + return true + } + + return false + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) { + var changed = false + var count = 0 + + count = incoming_events.count + incoming_events = incoming_events.filter(isIncluded) + changed = changed || incoming_events.count != count + + count = profile_zaps.zaps.count + profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) } + changed = changed || profile_zaps.zaps.count != count + + for el in reactions { + count = el.value.events.count + el.value.events = el.value.events.filter(isIncluded) + changed = changed || el.value.events.count != count + } + + for el in reposts { + count = el.value.events.count + el.value.events = el.value.events.filter(isIncluded) + changed = changed || el.value.events.count != count + } + + for el in zaps { + count = el.value.zaps.count + el.value.zaps = el.value.zaps.filter { + isIncluded($0.request.ev) + } + changed = changed || el.value.zaps.count != count + } + + count = replies.count + replies = replies.filter(isIncluded) + changed = changed || replies.count != count + + if changed { + self.notifications = build_notifications() + } + } + + func flush() -> Bool { + var inserted = false + + for zap in incoming_zaps { + inserted = insert_zap_immediate(zap) || inserted + } + + for event in incoming_events { + inserted = insert_event_immediate(event) || inserted + } + + if inserted { + self.notifications = build_notifications() + } + + return inserted + } +} diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index 0110de39..898f4b42 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -76,7 +76,7 @@ class SearchHomeModel: ObservableObject { // global events are not realtime unsubscribe(to: relay_id) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state) } @@ -98,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [ return Array(pubkeys) } + +func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] { + switch load { + case .from_events(let events): + return find_profiles_to_fetch_from_events(profiles: profiles, events: events) + case .from_keys(let pks): + return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks) + } +} + +func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] { + var pubkeys = Set() -func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] { + for pk in pks { + if profiles.lookup(id: pk) != nil { + continue + } + + pubkeys.insert(pk) + } + + return Array(pubkeys) +} + +func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] { var pubkeys = Set() for ev in events { @@ -113,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String return Array(pubkeys) } -func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) { +enum PubkeysToLoad { + case from_events([NostrEvent]) + case from_keys([String]) +} + +func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) { var filter = NostrFilter.filter_profiles - let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events) + let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load) filter.authors = authors guard !authors.isEmpty else { diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index fcbbf070..4fd44682 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -207,7 +207,7 @@ class ThreadModel: ObservableObject { } if sub_id == self.base_subid { - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state) + load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state) } } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift index 91f099da..0b726b2e 100644 --- a/damus/Models/ZapsModel.swift +++ b/damus/Models/ZapsModel.swift @@ -50,14 +50,14 @@ class ZapsModel: ObservableObject { break case .eose: let events = self.zaps.map { $0.request.ev } - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state) case .event(_, let ev): guard ev.kind == 9735 else { return } if let zap = state.zaps.zaps[ev.id] { - if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) { + if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) { objectWillChange.send() } } else { @@ -71,7 +71,7 @@ class ZapsModel: ObservableObject { state.zaps.add_zap(zap: zap) - if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) { + if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) { objectWillChange.send() } } diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift index 268949da..e068d27b 100644 --- a/damus/Nostr/Nostr.swift +++ b/damus/Nostr/Nostr.swift @@ -141,6 +141,9 @@ struct Profile: Codable { } static func displayName(profile: Profile?, pubkey: String) -> String { + if pubkey == "anon" { + return "Anonymous" + } let pk = bech32_nopre_pubkey(pubkey) ?? pubkey return profile?.name ?? abbrev_pubkey(pk) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index f9a66edf..826d3059 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -168,6 +168,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has return decrypted(privkey: privkey) ?? "*failed to decrypt content*" } + return content + + /* switch validity { case .ok: return content @@ -176,6 +179,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has case .bad_sig: return content + "\n\n*WARNING: invalid signature, could be forged!*" } + */ } var description: String { @@ -573,14 +577,25 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] { } } -func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent { +func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget, is_anon: Bool) -> NostrEvent { var tags = zap_target_to_tags(target) var relay_tag = ["relays"] relay_tag.append(contentsOf: relays.map { $0.url.absoluteString }) tags.append(relay_tag) - let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags) + + var priv = privkey + var pub = pubkey + + if is_anon { + tags.append(["anon"]) + let kp = generate_new_keypair() + pub = kp.pubkey + priv = kp.privkey! + } + + let ev = NostrEvent(content: content, pubkey: pub, kind: 9734, tags: tags) ev.id = calculate_event_id(ev: ev) - ev.sig = sign_event(privkey: privkey, ev: ev) + ev.sig = sign_event(privkey: priv, ev: ev) return ev } @@ -835,7 +850,7 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? { extension [ReferencedId] { var pRefs: [ReferencedId] { get { - self.filter { ref in + Set(self).filter { ref in ref.key == "p" } } diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift index 16b534bc..3d02ca87 100644 --- a/damus/Nostr/NostrKind.swift +++ b/damus/Nostr/NostrKind.swift @@ -21,4 +21,5 @@ enum NostrKind: Int { case chat = 42 case list = 30000 case zap = 9735 + case zap_request = 9734 } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift new file mode 100644 index 00000000..2a898ea3 --- /dev/null +++ b/damus/Util/EventCache.swift @@ -0,0 +1,27 @@ +// +// EventCache.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class EventCache { + private var events: [String: NostrEvent] + + func lookup(_ evid: String) -> NostrEvent? { + return events[evid] + } + + func insert(_ ev: NostrEvent) { + guard events[ev.id] == nil else { + return + } + events[ev.id] = ev + } + + init() { + self.events = [:] + } +} diff --git a/damus/Util/EventHolder.swift b/damus/Util/EventHolder.swift index aea546a4..58e0a221 100644 --- a/damus/Util/EventHolder.swift +++ b/damus/Util/EventHolder.swift @@ -8,11 +8,15 @@ import Foundation /// Used for holding back events until they're ready to be displayed -class EventHolder: ObservableObject { +class EventHolder: ObservableObject, ScrollQueue { private var has_event: Set @Published var events: [NostrEvent] @Published var incoming: [NostrEvent] - @Published var should_queue: Bool + var should_queue: Bool + + func set_should_queue(_ val: Bool) { + self.should_queue = val + } var queued: Int { return incoming.count diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift index 55768c34..86be0a3c 100644 --- a/damus/Util/InsertSort.swift +++ b/damus/Util/InsertSort.swift @@ -38,8 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp: return true } -@discardableResult -func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { +func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool { var i: Int = 0 for zap in zaps { @@ -48,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { return false } - if new_zap.invoice.amount > zap.invoice.amount { + if cmp(new_zap, zap) { zaps.insert(new_zap, at: i) return true } @@ -59,6 +58,19 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { return true } +@discardableResult +func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool { + return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in + a.event.created_at > b.event.created_at + } +} + +@discardableResult +func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool { + return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in + a.invoice.amount > b.invoice.amount + } +} func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool { return insert_uniq_sorted_event(events: &events, new_ev: new_ev) { diff --git a/damus/Util/LNUrlPayRequest.swift b/damus/Util/LNUrlPayRequest.swift index 610d6554..f45c5956 100644 --- a/damus/Util/LNUrlPayRequest.swift +++ b/damus/Util/LNUrlPayRequest.swift @@ -9,8 +9,10 @@ import Foundation struct LNUrlPayRequest: Decodable { let allowsNostr: Bool? + let commentAllowed: Int? let nostrPubkey: String? + let metadata: String? let minSendable: Int64? let maxSendable: Int64? let status: String? diff --git a/damus/Util/LocalizationUtil.swift b/damus/Util/LocalizationUtil.swift new file mode 100644 index 00000000..7e2aa938 --- /dev/null +++ b/damus/Util/LocalizationUtil.swift @@ -0,0 +1,17 @@ +// +// LocalizationUtil.swift +// damus +// +// Created by Terry Yiu on 2/24/23. +// + +import Foundation + +func bundleForLocale(locale: Locale?) -> Bundle { + if locale == nil { + return Bundle.main + } + + let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj") + return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main +} diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift index ff08c49f..bbe9334f 100644 --- a/damus/Util/Notifications.swift +++ b/damus/Util/Notifications.swift @@ -104,6 +104,9 @@ extension Notification.Name { static var update_bookmarks: Notification.Name { return Notification.Name("update_bookmarks") } + static var zapping: Notification.Name { + return Notification.Name("zapping") + } } func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { diff --git a/damus/Util/TimeAgo.swift b/damus/Util/TimeAgo.swift index 70626fb2..15917cbd 100644 --- a/damus/Util/TimeAgo.swift +++ b/damus/Util/TimeAgo.swift @@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current return formatter.string(from: DateComponents(calendar: calendar, second: second))! } - return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred") + let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current) + return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred") } diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift index 245040ff..02d3d63d 100644 --- a/damus/Util/Zap.swift +++ b/damus/Util/Zap.swift @@ -285,7 +285,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { return endpoint } -func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) async -> String? { +func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { return nil } @@ -295,12 +295,18 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) var query = [URLQueryItem(name: "amount", value: "\(amount)")] - if zappable { + if zappable && zap_type != .non_zap { if let json = encode_json(zapreq) { print("zapreq json: \(json)") query.append(URLQueryItem(name: "nostr", value: json)) } } + + // add a lud12 comment as well if we have it + if let comment, let limit = payreq.commentAllowed, limit != 0 { + let limited_comment = String(comment.prefix(limit)) + query.append(URLQueryItem(name: "comment", value: limited_comment)) + } base_url.queryItems = query diff --git a/damus/Util/Zaps.swift b/damus/Util/Zaps.swift index f75abb64..5d6b8e67 100644 --- a/damus/Util/Zaps.swift +++ b/damus/Util/Zaps.swift @@ -36,7 +36,7 @@ class Zaps { if our_zaps[note_target.note_id] == nil { our_zaps[note_target.note_id] = [zap] } else { - insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap) + insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap) } case .profile(_): break diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift index f4180706..799f0925 100644 --- a/damus/Views/ActionBar/EventDetailBar.swift +++ b/damus/Views/ActionBar/EventDetailBar.swift @@ -26,14 +26,16 @@ struct EventDetailBar: View { HStack { if bar.boosts > 0 { NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) { - Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") + let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray) + Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") } .buttonStyle(PlainButtonStyle()) } if bar.likes > 0 { NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) { - Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") + let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray) + Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") } .buttonStyle(PlainButtonStyle()) } @@ -41,7 +43,8 @@ struct EventDetailBar: View { if bar.zaps > 0 { let dst = ZapsView(state: state, target: .note(id: target, author: target_pk)) NavigationLink(destination: dst) { - Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.") + let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray) + Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.") } .buttonStyle(PlainButtonStyle()) } @@ -49,6 +52,21 @@ struct EventDetailBar: View { } } +func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "reposts_count", value: nil, table: nil), locale: locale, count) +} + +func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "reactions_count", value: nil, table: nil), locale: locale, count) +} + +func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "zaps_count", value: nil, table: nil), locale: locale, count) +} + struct EventDetailBar_Previews: PreviewProvider { static var previews: some View { EventDetailBar(state: test_damus_state(), target: "", target_pk: "") diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift index f2adb061..f88840f1 100644 --- a/damus/Views/ConfigView.swift +++ b/damus/Views/ConfigView.swift @@ -129,26 +129,14 @@ struct ConfigView: View { } } - Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) { TextField(String("1000"), text: $default_zap_amount) .keyboardType(.numberPad) .onReceive(Just(default_zap_amount)) { newValue in - let filtered = newValue.filter { Set("0123456789").contains($0) } - - if filtered != newValue { - default_zap_amount = filtered + + if let parsed = handle_string_amount(new_value: newValue) { + self.default_zap_amount = String(parsed) } - - if filtered == "" { - set_default_zap_amount(pubkey: state.pubkey, amount: 1000) - return - } - - guard let amt = Int(filtered) else { - return - } - set_default_zap_amount(pubkey: state.pubkey, amount: amt) } } @@ -220,10 +208,10 @@ struct ConfigView: View { } } - let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String - let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String - Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) { - Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))") + if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] { + Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) { + Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))") + } } } } @@ -346,3 +334,18 @@ struct ConfigView_Previews: PreviewProvider { } } } + + +func handle_string_amount(new_value: String) -> Int? { + let filtered = new_value.filter { Set("0123456789").contains($0) } + + if filtered == "" { + return nil + } + + guard let amt = Int(filtered) else { + return nil + } + + return amt +} diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 0910f44d..7d24355b 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -37,9 +37,7 @@ struct DMChatView: View { var Header: some View { let profile = damus_state.profiles.lookup(id: pubkey) - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let fmodel = FollowersModel(damus_state: damus_state, target: pubkey) - let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel) + let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey) return NavigationLink(destination: profile_page) { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles) diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift index 675eb51b..d12232d6 100644 --- a/damus/Views/DirectMessagesView.swift +++ b/damus/Views/DirectMessagesView.swift @@ -41,6 +41,9 @@ struct DirectMessagesView: View { ForEach(dms, id: \.0) { tup in MaybeEvent(tup) .padding(.top, 10) + + Divider() + .padding([.top], 10) } } } diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift index d8654392..18999bee 100644 --- a/damus/Views/EventDetailView.swift +++ b/damus/Views/EventDetailView.swift @@ -23,7 +23,7 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) { struct EventDetailView_Previews: PreviewProvider { static var previews: some View { - let state = test_damus_state() + let _ = test_damus_state() EventDetailView() } } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index 689fe71f..7c8985a5 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -61,10 +61,8 @@ struct EventView: View { if event.known_kind == .boost { if let inner_ev = event.inner_event { VStack(alignment: .leading) { - let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus) - let follow_model = FollowersModel(damus_state: damus, target: event.pubkey) let prof = damus.profiles.lookup(id: event.pubkey) - let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model) + let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey) NavigationLink(destination: booster_profile) { Reposted(damus: damus, pubkey: event.pubkey, profile: prof) @@ -86,9 +84,6 @@ struct EventView: View { TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil) .padding([.top], 6) } - - Divider() - .padding([.top], 4) } } } diff --git a/damus/Views/Events/BuilderEventView.swift b/damus/Views/Events/BuilderEventView.swift index 26c243b7..9cb0666f 100644 --- a/damus/Views/Events/BuilderEventView.swift +++ b/damus/Views/Events/BuilderEventView.swift @@ -71,8 +71,10 @@ struct BuilderEventView: View { } } .frame(minWidth: 0, maxWidth: .infinity) - .cornerRadius(8) - .border(Color.gray.opacity(0.2), width: 1) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray.opacity(0.2), lineWidth: 1.0) + ) .onAppear { self.load() } diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift index 65c5ead6..26efd06c 100644 --- a/damus/Views/Events/EventBody.swift +++ b/damus/Views/Events/EventBody.swift @@ -11,6 +11,14 @@ struct EventBody: View { let damus_state: DamusState let event: NostrEvent let size: EventViewKind + let should_show_img: Bool + + init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil) { + self.damus_state = damus_state + self.event = event + self.size = size + self.should_show_img = should_show_img ?? should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + } var content: String { event.get_content(damus_state.keypair.privkey) @@ -21,8 +29,6 @@ struct EventBody: View { ReplyDescription(event: event, profiles: damus_state.profiles) } - let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil) - NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content)) .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index da375fbc..1669241a 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -47,9 +47,9 @@ struct EventMenuContext: View { notify(.update_bookmarks, event) } label: { let imageName = isBookmarked ? "bookmark.fill" : "bookmark" - let unBookmarkString = NSLocalizedString("Un-Bookmark", comment: "Context menu option for un-bookmarking a note") - let bookmarkString = NSLocalizedString("Bookmark", comment: "Context menu optoin for bookmarking a note") - Label(isBookmarked ? unBookmarkString : bookmarkString, systemImage: imageName) + let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.") + let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.") + Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName) } .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift index 17ef625e..fc2e6734 100644 --- a/damus/Views/Events/EventProfile.swift +++ b/damus/Views/Events/EventProfile.swift @@ -31,10 +31,7 @@ struct EventProfile: View { var body: some View { HStack(alignment: .center) { VStack { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey)) - - NavigationLink(destination: pv) { + NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) { ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles) } } diff --git a/damus/Views/Events/ReplyDescription.swift b/damus/Views/Events/ReplyDescription.swift index 7e130d62..fa8f471a 100644 --- a/damus/Views/Events/ReplyDescription.swift +++ b/damus/Views/Events/ReplyDescription.swift @@ -26,13 +26,15 @@ struct ReplyDescription_Previews: PreviewProvider { } } -func reply_desc(profiles: Profiles, event: NostrEvent) -> String { +func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.current) -> String { let desc = make_reply_description(event.tags) let pubkeys = desc.pubkeys let n = desc.others + let bundle = bundleForLocale(locale: locale) + if desc.pubkeys.count == 0 { - return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.") + return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.") } let names: [String] = pubkeys.map { @@ -40,20 +42,16 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String { return Profile.displayName(profile: prof, pubkey: $0) } - let othersCount = n - pubkeys.count if names.count > 1 { + let othersCount = n - pubkeys.count if othersCount == 0 { - return String(format: "Replying to %@ & %@", names[0], names[1]) + return String(format: NSLocalizedString("Replying to %@ & %@", bundle: bundle, comment: "Label to indicate that the user is replying to 2 users."), locale: locale, names[0], names[1]) } else { - return String(format: "Replying to %@, %@ & %d others", names[0], names[1], othersCount) + return String(format: bundle.localizedString(forKey: "replying_to_two_and_others", value: nil, table: nil), locale: locale, othersCount, names[0], names[1]) } } - if othersCount == 0 { - return String(format: "Replying to %@", names[0]) - } else { - return String(format: "Replying to %@ & %d others", names[0], othersCount) - } + return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, names[0]) } diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift index 2c6889f6..9c27e293 100644 --- a/damus/Views/Events/TextEvent.swift +++ b/damus/Views/Events/TextEvent.swift @@ -18,20 +18,17 @@ struct TextEvent: View { HStack(alignment: .top) { let profile = damus.profiles.lookup(id: pubkey) + let is_anon = event_is_anonymous(ev: event) VStack { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus) - let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) - - NavigationLink(destination: pv) { - ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles) - } + MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey) Spacer() } VStack(alignment: .leading) { HStack(alignment: .center) { - EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal) + let pk = is_anon ? "anon" : pubkey + EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal) Text(verbatim: "\(format_relative_time(event.created_at))") .foregroundColor(.gray) @@ -68,3 +65,18 @@ struct TextEvent_Previews: PreviewProvider { TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil) } } + +func event_has_tag(ev: NostrEvent, tag: String) -> Bool { + for t in ev.tags { + if t.count >= 1 && t[0] == tag { + return true + } + } + + return false +} + + +func event_is_anonymous(ev: NostrEvent) -> Bool { + return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon") +} diff --git a/damus/Views/FollowButtonView.swift b/damus/Views/FollowButtonView.swift index d79a17de..1e96c34c 100644 --- a/damus/Views/FollowButtonView.swift +++ b/damus/Views/FollowButtonView.swift @@ -19,7 +19,7 @@ struct FollowButtonView: View { Button { follow_state = perform_follow_btn_action(follow_state, target: target) } label: { - Text(follow_btn_txt(follow_state, follows_you: follows_you)) + Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))") .frame(width: 105, height: 30) //.padding(.vertical, 10) .font(.caption.weight(.bold)) diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift index 4628b2ce..41e9bfc6 100644 --- a/damus/Views/FollowingView.swift +++ b/damus/Views/FollowingView.swift @@ -29,7 +29,6 @@ struct FollowersView: View { @EnvironmentObject var followers: FollowersModel var body: some View { - let profile = damus_state.profiles.lookup(id: whos) ScrollView { LazyVStack(alignment: .leading) { ForEach(followers.contacts ?? [], id: \.self) { pk in @@ -38,7 +37,7 @@ struct FollowersView: View { } .padding() } - .navigationBarTitle(NSLocalizedString("\(Profile.displayName(profile: profile, pubkey: whos))'s Followers", comment: "Navigation bar title for view that shows who is following a user.")) + .navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user.")) .onAppear { followers.subscribe() } @@ -56,8 +55,6 @@ struct FollowingView: View { let whos: String var body: some View { - let profile = damus_state.profiles.lookup(id: whos) - let who = Profile.displayName(profile: profile, pubkey: whos) ScrollView { LazyVStack(alignment: .leading) { ForEach(following.contacts, id: \.self) { pk in @@ -72,7 +69,7 @@ struct FollowingView: View { .onDisappear { following.unsubscribe() } - .navigationBarTitle(NSLocalizedString("\(who) following", comment: "Navigation bar title for view that shows who a user is following.")) + .navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following.")) } } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift new file mode 100644 index 00000000..d2b466a3 --- /dev/null +++ b/damus/Views/Notifications/EventGroupView.swift @@ -0,0 +1,214 @@ +// +// RepostGroupView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + + +enum EventGroupType { + case repost(EventGroup) + case reaction(EventGroup) + case zap(ZapGroup) + case profile_zap(ZapGroup) + + var events: [NostrEvent] { + switch self { + case .repost(let grp): + return grp.events + case .reaction(let grp): + return grp.events + case .zap(let zapgrp): + return zapgrp.zap_requests() + case .profile_zap(let zapgrp): + return zapgrp.zap_requests() + } + } +} + +enum ReactingTo { + case your_post + case tagged_in + case your_profile +} + +func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo { + guard let ev else { + return .your_profile + } + + if ev.pubkey == our_pubkey { + return .your_post + } + + return .tagged_in +} + +func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String { + let alice_pk = ev.pubkey + let alice_prof = profiles.lookup(id: alice_pk) + return Profile.displayName(profile: alice_prof, pubkey: alice_pk) +} + +/** + Returns a notification string describing user actions in response to an event group type. + + The localization keys read by this function are the following (although some keys may not actually be used in practice): + + "??" - returned when there are no events associated with the specified event group type. + + "reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in + "reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in + "reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in + "reacted_your_post_1" - returned when 1 reaction occurred to the current user's post + "reacted_your_post_2" - returned when 2 reactions occurred to the current user's post + "reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post + "reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile + "reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile + "reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile + + "reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in + "reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in + "reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in + "reposted_your_post_1" - returned when 1 repost occurred to the current user's post + "reposted_your_post_2" - returned when 2 reposts occurred to the current user's post + "reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post + "reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile + "reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile + "reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile + + "zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in + "zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in + "zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in + "zapped_your_post_1" - returned when 1 zap occurred to the current user's post + "zapped_your_post_2" - returned when 2 zaps occurred to the current user's post + "zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post + "zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile + "zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile + "zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile + */ +func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String { + let verb = reacting_to_verb(group: group) + let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev) + let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))" + let bundle = bundleForLocale(locale: locale) + + switch group.events.count { + case 0: + return NSLocalizedString("??", comment: "") + case 1: + let ev = group.events.first! + let profile = profiles.lookup(id: ev.pubkey) + let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey) + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name) + case 2: + let alice_name = event_author_name(profiles: profiles, group.events[0]) + let bob_name = event_author_name(profiles: profiles, group.events[1]) + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name) + default: + let alice_name = event_author_name(profiles: profiles, group.events.first!) + let count = group.events.count - 1 + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name) + } +} + +func reacting_to_verb(group: EventGroupType) -> String { + switch group { + case .reaction: + return "reacted" + case .repost: + return "reposted" + case .zap: fallthrough + case .profile_zap: + return "zapped" + } +} + +struct EventGroupView: View { + let state: DamusState + let event: NostrEvent? + let group: EventGroupType + + var GroupDescription: some View { + Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))") + } + + func ZapIcon(_ zapgrp: ZapGroup) -> some View { + let fmt = format_msats_abbrev(zapgrp.msat_total) + return VStack(alignment: .center) { + Image(systemName: "bolt.fill") + .foregroundColor(.orange) + Text("\(fmt)") + .foregroundColor(Color.orange) + } + } + + var GroupIcon: some View { + Group { + switch group { + case .repost: + Image(systemName: "arrow.2.squarepath") + .foregroundColor(Color("DamusGreen")) + case .reaction: + Image("shaka-full") + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.accentColor) + case .profile_zap(let zapgrp): + ZapIcon(zapgrp) + case .zap(let zapgrp): + ZapIcon(zapgrp) + } + } + } + + var body: some View { + HStack(alignment: .top) { + GroupIcon + .frame(width: PFP_SIZE + 10) + + VStack(alignment: .leading) { + ProfilePicturesView(state: state, events: group.events) + + GroupDescription + + if let event { + NavigationLink(destination: BuildThreadV2View(damus: state, event_id: event.id)) { + Text(event.content) + .padding([.top], 1) + .foregroundColor(.gray) + } + .buttonStyle(.plain) + } + } + } + .padding([.top], 6) + } +} + +let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" +let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1) +let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1) +let test_reposts = [test_repost_1, test_repost_2] +let test_event_group = EventGroup(events: test_reposts) + +struct EventGroupView_Previews: PreviewProvider { + static var previews: some View { + VStack { + EventGroupView(state: test_damus_state(), event: test_event, group: .repost(test_event_group)) + .frame(height: 200) + .padding() + + EventGroupView(state: test_damus_state(), event: test_event, group: .reaction(test_event_group)) + .frame(height: 200) + .padding() + } + } + +} + diff --git a/damus/Views/Notifications/NotificationItemView.swift b/damus/Views/Notifications/NotificationItemView.swift new file mode 100644 index 00000000..9d131ac5 --- /dev/null +++ b/damus/Views/Notifications/NotificationItemView.swift @@ -0,0 +1,86 @@ +// +// NotificationItemView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + +enum ShowItem { + case show(NostrEvent?) + case dontshow(NostrEvent?) +} + +func notification_item_event(events: EventCache, notif: NotificationItem) -> ShowItem { + switch notif { + case .repost(let evid, _): + return .dontshow(events.lookup(evid)) + case .reply(let ev): + return .show(ev) + case .reaction(let evid, _): + return .dontshow(events.lookup(evid)) + case .event_zap(let evid, _): + return .dontshow(events.lookup(evid)) + case .profile_zap: + return .show(nil) + } +} + +struct NotificationItemView: View { + let state: DamusState + let item: NotificationItem + + var show_item: ShowItem { + notification_item_event(events: state.events, notif: item) + } + + func Item(_ ev: NostrEvent?) -> some View { + Group { + switch item { + case .repost(_, let evgrp): + EventGroupView(state: state, event: ev, group: .repost(evgrp)) + + case .event_zap(_, let zapgrp): + EventGroupView(state: state, event: ev, group: .zap(zapgrp)) + + case .profile_zap(let grp): + EventGroupView(state: state, event: nil, group: .profile_zap(grp)) + + case .reaction(_, let evgrp): + EventGroupView(state: state, event: ev, group: .reaction(evgrp)) + + case .reply(let ev): + NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) { + EventView(damus: state, event: ev, has_action_bar: true) + } + .buttonStyle(.plain) + } + + Divider() + .padding([.top,.bottom], 5) + } + } + + var body: some View { + Group { + switch show_item { + case .show(let ev): + Item(ev) + + case .dontshow(let ev): + if let ev { + Item(ev) + } + } + } + } +} + +let test_notification_item: NotificationItem = .repost("evid", test_event_group) + +struct NotificationItemView_Previews: PreviewProvider { + static var previews: some View { + NotificationItemView(state: test_damus_state(), item: test_notification_item) + } +} diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift new file mode 100644 index 00000000..16c561c1 --- /dev/null +++ b/damus/Views/Notifications/NotificationsView.swift @@ -0,0 +1,50 @@ +// +// NotificationsView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + +struct NotificationsView: View { + let state: DamusState + @ObservedObject var notifications: NotificationsModel + + var body: some View { + ScrollViewReader { scroller in + ScrollView { + LazyVStack(alignment: .leading) { + Color.white.opacity(0) + .id("startblock") + .frame(height: 5) + ForEach(notifications.notifications, id: \.id) { item in + NotificationItemView(state: state, item: item) + } + } + .background(GeometryReader { proxy -> Color in + DispatchQueue.main.async { + handle_scroll_queue(proxy, queue: self.notifications) + } + return Color.clear + }) + .padding(.horizontal) + } + .coordinateSpace(name: "scroll") + .onReceive(handle_notify(.scroll_to_top)) { notif in + let _ = notifications.flush() + self.notifications.should_queue = false + scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top) + } + } + .onAppear { + let _ = notifications.flush() + } + } +} + +struct NotificationsView_Previews: PreviewProvider { + static var previews: some View { + NotificationsView(state: test_damus_state(), notifications: NotificationsModel()) + } +} diff --git a/damus/Views/Notifications/ProfilePicturesView.swift b/damus/Views/Notifications/ProfilePicturesView.swift new file mode 100644 index 00000000..2991d6ec --- /dev/null +++ b/damus/Views/Notifications/ProfilePicturesView.swift @@ -0,0 +1,37 @@ +// +// ProfilePicturesView.swift +// damus +// +// Created by William Casarin on 2023-02-22. +// + +import SwiftUI + +struct ProfilePicturesView: View { + let state: DamusState + let events: [NostrEvent] + + @State var nav_target: String? = nil + @State var navigating: Bool = false + + var body: some View { + NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) { + EmptyView() + } + HStack { + ForEach(events.prefix(8)) { ev in + ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles) + .onTapGesture { + nav_target = ev.pubkey + navigating = true + } + } + } + } +} + +struct ProfilePicturesView_Previews: PreviewProvider { + static var previews: some View { + ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event]) + } +} diff --git a/damus/Views/ParicipantsView.swift b/damus/Views/ParicipantsView.swift index 720a3e84..cf29f8d8 100644 --- a/damus/Views/ParicipantsView.swift +++ b/damus/Views/ParicipantsView.swift @@ -36,41 +36,43 @@ struct ParticipantsView: View { Spacer() } VStack { - ForEach(originalReferences.pRefs) { participant in - let pubkey = participant.id - HStack { - ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) - - VStack(alignment: .leading) { - let profile = damus_state.profiles.lookup(id: pubkey) - ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false) - if let about = profile?.about { - Text(FollowUserView.markdown.process(about)) - .lineLimit(3) - .font(.footnote) + ScrollView { + ForEach(originalReferences.pRefs) { participant in + let pubkey = participant.id + HStack { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) + + VStack(alignment: .leading) { + let profile = damus_state.profiles.lookup(id: pubkey) + ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false) + if let about = profile?.about { + Text(FollowUserView.markdown.process(about)) + .lineLimit(3) + .font(.footnote) + } } + + Spacer() + + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 30)) + .foregroundColor(references.contains(participant) ? .purple : .gray) } - - Spacer() - - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 30)) - .foregroundColor(references.contains(participant) ? .purple : .gray) - } - .onTapGesture { - if references.contains(participant) { - references = references.filter { - $0 != participant - } - } else { + .onTapGesture { if references.contains(participant) { - // Don't add it twice + references = references.filter { + $0 != participant + } } else { - references.append(participant) + if references.contains(participant) { + // Don't add it twice + } else { + references.append(participant) + } } } - } - } + } + } } Spacer() } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 3ca4d14a..dfa2356e 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -15,7 +15,7 @@ enum NostrPostResult { let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.") struct PostView: View { - @State var post: String = "" + @State var post: NSMutableAttributedString = NSMutableAttributedString() @FocusState var focus: Bool @State var showPrivateKeyWarning: Bool = false @@ -44,7 +44,14 @@ struct PostView: View { if replying_to?.known_kind == .chat { kind = .chat } - let content = self.post.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in + if let link = attributes[.link] as? String { + post.replaceCharacters(in: range, with: link) + } + } + + let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let new_post = NostrPost(content: content, references: references, kind: kind) NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post)) @@ -52,14 +59,14 @@ struct PostView: View { if let replying_to { damus_state.drafts.replies.removeValue(forKey: replying_to) } else { - damus_state.drafts.post = "" + damus_state.drafts.post = NSMutableAttributedString(string: "") } dismiss() } var is_post_empty: Bool { - return post.allSatisfy { $0.isWhitespace } + return post.string.allSatisfy { $0.isWhitespace } } var body: some View { @@ -74,7 +81,7 @@ struct PostView: View { if !is_post_empty { Button(NSLocalizedString("Post", comment: "Button to post a note.")) { - showPrivateKeyWarning = contentContainsPrivateKey(self.post) + showPrivateKeyWarning = contentContainsPrivateKey(self.post.string) if !showPrivateKeyWarning { self.send_post() @@ -97,7 +104,7 @@ struct PostView: View { VStack(alignment: .leading) { ZStack(alignment: .topLeading) { - TextEditor(text: $post) + TextViewWrapper(attributedText: $post) .focused($focus) .textInputAutocapitalization(.sentences) .onChange(of: post) { _ in @@ -108,7 +115,7 @@ struct PostView: View { } } - if post.isEmpty { + if post.string.isEmpty { Text(POST_PLACEHOLDER) .padding(.top, 8) .padding(.leading, 4) @@ -120,7 +127,7 @@ struct PostView: View { } // This if-block observes @ for tagging - if let searching = get_searching_string(post) { + if let searching = get_searching_string(post.string) { VStack { Spacer() UserSearch(damus_state: damus_state, search: searching, post: $post) @@ -130,7 +137,7 @@ struct PostView: View { .onAppear() { if let replying_to { if damus_state.drafts.replies[replying_to] == nil { - damus_state.drafts.replies[replying_to] = "" + damus_state.drafts.post = NSMutableAttributedString(string: "") } if let p = damus_state.drafts.replies[replying_to] { post = p @@ -144,10 +151,10 @@ struct PostView: View { } } .onDisappear { - if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { damus_state.drafts.replies.removeValue(forKey: replying_to) - } else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - damus_state.drafts.post = "" + } else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + damus_state.drafts.post = NSMutableAttributedString(string : "") } } .padding() diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift index a0ac99f4..072857db 100644 --- a/damus/Views/Posting/UserSearch.swift +++ b/damus/Views/Posting/UserSearch.swift @@ -20,7 +20,8 @@ struct SearchedUser: Identifiable { struct UserSearch: View { let damus_state: DamusState let search: String - @Binding var post: String + + @Binding var post: NSMutableAttributedString var users: [SearchedUser] { guard let contacts = damus_state.contacts.event else { @@ -39,7 +40,26 @@ struct UserSearch: View { guard let pk = bech32_pubkey(user.pubkey) else { return } - post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ") + + while post.string.last != "@" { + post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + } + post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + + + var tagString = "" + if let name = user.profile?.name { + tagString = "@\(name)\u{200B} " + } + let tagAttributedString = NSMutableAttributedString(string: tagString, + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), + NSAttributedString.Key.link: "@\(pk)"]) + tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2)) + tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2)) + let mutableString = NSMutableAttributedString() + mutableString.append(post) + mutableString.append(tagAttributedString) + post = mutableString } } } @@ -49,7 +69,7 @@ struct UserSearch: View { struct UserSearch_Previews: PreviewProvider { static let search: String = "jb55" - @State static var post: String = "some @jb55" + @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") static var previews: some View { UserSearch(damus_state: test_damus_state(), search: search, post: $post) diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift new file mode 100644 index 00000000..19d8f269 --- /dev/null +++ b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -0,0 +1,46 @@ +// +// MaybeAnonPfpView.swift +// damus +// +// Created by William Casarin on 2023-02-26. +// + +import SwiftUI + +struct MaybeAnonPfpView: View { + let state: DamusState + let is_anon: Bool + let pubkey: String + + init(state: DamusState, event: NostrEvent, pubkey: String) { + self.state = state + self.is_anon = event_is_anonymous(ev: event) + self.pubkey = pubkey + } + + init(state: DamusState, is_anon: Bool, pubkey: String) { + self.state = state + self.is_anon = is_anon + self.pubkey = pubkey + } + + var body: some View { + Group { + if is_anon { + Image(systemName: "person.fill.questionmark") + .font(.largeTitle) + .frame(width: PFP_SIZE, height: PFP_SIZE) + } else { + NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles) + } + } + } + } +} + +struct MaybeAnonPfpView_Previews: PreviewProvider { + static var previews: some View { + MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon") + } +} diff --git a/damus/Views/ProfileName.swift b/damus/Views/ProfileName.swift index 9e8d6be8..dfabc9d5 100644 --- a/damus/Views/ProfileName.swift +++ b/damus/Views/ProfileName.swift @@ -67,7 +67,7 @@ struct ProfileName: View { var body: some View { HStack(spacing: 2) { - Text(prefix + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) + Text(verbatim: "\(prefix)\(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))") .font(.body) .fontWeight(prefix == "@" ? .none : .bold) if let nip05 = current_nip05 { @@ -136,11 +136,11 @@ struct EventProfileName: View { .font(.body.weight(.bold)) .padding([.trailing], 2) - Text("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) + Text(verbatim: "@\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))") .foregroundColor(Color("DamusMediumGrey")) .font(eventviewsize_to_font(size)) } else { - Text(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))) + Text(verbatim: "\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))") .font(eventviewsize_to_font(size)) .fontWeight(.bold) } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index 9828b207..8e4c3272 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -49,6 +49,16 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool { } } +func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "followers_count", value: nil, table: nil), locale: locale, count) +} + +func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String { + let bundle = bundleForLocale(locale: locale) + return String(format: bundle.localizedString(forKey: "relays_count", value: nil, table: nil), locale: locale, count) +} + struct EditButton: View { let damus_state: DamusState @@ -100,8 +110,6 @@ struct ProfileView: View { static let markdown = Markdown() @State private var selected_tab: ProfileTab = .posts - @StateObject var profile: ProfileModel - @StateObject var followers: FollowersModel @State private var showingEditProfile = false @State var showing_select_wallet: Bool = false @State var is_zoomed: Bool = false @@ -110,6 +118,21 @@ struct ProfileView: View { @State var filter_state : FilterState = .posts @State var yOffset: CGFloat = 0 + @StateObject var profile: ProfileModel + @StateObject var followers: FollowersModel + + init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: profile) + self._followers = StateObject(wrappedValue: followers) + } + + init(damus_state: DamusState, pubkey: String) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) + self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey)) + } + @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @Environment(\.openURL) var openURL @@ -319,7 +342,8 @@ struct ProfileView: View { .foregroundColor(.gray) } else { let followerCount = followers.count! - Text("\(Text(verbatim: "\(followerCount)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "followers_count", value: nil, table: nil), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") + let noun_text = Text(verbatim: "\(followersCountString(followerCount))").font(.subheadline).foregroundColor(.gray) + Text("\(Text("\(followerCount)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") } } } @@ -343,7 +367,8 @@ struct ProfileView: View { let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { HStack { - Text("\(Text(verbatim: "\(profile.following)").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") + let noun_text = Text("Following", comment: "Text on the user profile page next to the number of accounts a user is following.").font(.subheadline).foregroundColor(.gray) + Text("\(Text("\(profile.following)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") } } .buttonStyle(PlainButtonStyle()) @@ -366,7 +391,8 @@ struct ProfileView: View { if let relays = profile.relays { // Only open relay config view if the user is logged in with private key and they are looking at their own profile. - let relay_text = Text("\(Text(verbatim: "\(relays.keys.count)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "relays_count", value: nil, table: nil), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") + let noun_text = Text(verbatim: "\(relaysCountString(relays.keys.count))").font(.subheadline).foregroundColor(.gray) + let relay_text = Text("\(Text("\(relays.keys.count)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { NavigationLink(destination: RelayConfigView(state: damus_state)) { relay_text @@ -443,9 +469,7 @@ struct ProfileView: View { struct ProfileView_Previews: PreviewProvider { static var previews: some View { let ds = test_damus_state() - let followers = FollowersModel(damus_state: ds, target: ds.pubkey) - let profile_model = ProfileModel(pubkey: ds.pubkey, damus: ds) - ProfileView(damus_state: ds, profile: profile_model, followers: followers) + ProfileView(damus_state: ds, pubkey: ds.pubkey) } } @@ -506,7 +530,7 @@ struct KeyView: View { .symbolRenderingMode(.palette) } .padding(.leading,4) - Text(abbrev_pubkey(bech32, amount: 16)) + Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") .font(.footnote) .foregroundColor(keyColor()) } diff --git a/damus/Views/RelayFilterView.swift b/damus/Views/RelayFilterView.swift index 8f99b5b8..d3f9efcf 100644 --- a/damus/Views/RelayFilterView.swift +++ b/damus/Views/RelayFilterView.swift @@ -26,7 +26,7 @@ struct RelayFilterView: View { } var body: some View { - Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.") + Text("Please choose relays from the list below to filter the current feed:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.") .padding() .padding(.top, 20) .padding(.bottom, 0) diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift index e2de28ab..875dc8b4 100644 --- a/damus/Views/SaveKeysView.swift +++ b/damus/Views/SaveKeysView.swift @@ -38,7 +38,7 @@ struct SaveKeysView: View { .foregroundColor(.white) .padding(.bottom, 10) - Text("This is your account ID, you can give this to your friends so that they can follow you. Click to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.") + Text("This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.") .foregroundColor(.white) .padding(.bottom, 10) diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index f1fea5d4..add412d6 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -108,6 +108,7 @@ struct SideMenuView: View { navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear") } } + .labelStyle(SideMenuLabelStyle()) .padding([.top, .bottom], verticalSpacing) } } @@ -175,6 +176,17 @@ struct SideMenuView: View { .foregroundColor(textColor()) .frame(maxWidth: .infinity, alignment: .leading) } + + struct SideMenuLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(alignment: .center, spacing: 8) { + configuration.icon + .frame(width: 24, height: 24) + .aspectRatio(contentMode: .fit) + configuration.title + } + } + } } struct Previews_SideMenuView_Previews: PreviewProvider { diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift new file mode 100644 index 00000000..15d10c2a --- /dev/null +++ b/damus/Views/TextViewWrapper.swift @@ -0,0 +1,44 @@ +// +// TextViewWrapper.swift +// damus +// +// Created by Swift on 2/24/23. +// + +import SwiftUI + +struct TextViewWrapper: UIViewRepresentable { + @Binding var attributedText: NSMutableAttributedString + + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.delegate = context.coordinator + textView.font = UIFont.systemFont(ofSize: 18) + textView.textColor = UIColor.label + let linkAttributes: [NSAttributedString.Key : Any] = [ + NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)] + textView.linkTextAttributes = linkAttributes + return textView + } + + func updateUIView(_ uiView: UITextView, context: Context) { + uiView.attributedText = attributedText + } + + func makeCoordinator() -> Coordinator { + Coordinator(attributedText: $attributedText) + } + + class Coordinator: NSObject, UITextViewDelegate { + @Binding var attributedText: NSMutableAttributedString + + init(attributedText: Binding) { + _attributedText = attributedText + } + + func textViewDidChange(_ textView: UITextView) { + attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + } + } +} + diff --git a/damus/Views/ThreadV2View.swift b/damus/Views/ThreadV2View.swift index 94038f3c..f35a4adf 100644 --- a/damus/Views/ThreadV2View.swift +++ b/damus/Views/ThreadV2View.swift @@ -265,6 +265,10 @@ struct ThreadV2View: View { navigating: $navigating, selected: false ) + + Divider() + .padding(.top, 4) + .padding(.leading, 25 * 2) } }.background(GeometryReader { geometry in // get the height and width of the EventView view @@ -289,15 +293,20 @@ struct ThreadV2View: View { ).id("main") // MARK: - Responses of the actual event view - ForEach(thread.childEvents, id: \.id) { event in - MutedEventView( - damus_state: damus, - event: event, - scroller: reader, - nav_target: $nav_target, - navigating: $navigating, - selected: false - ) + LazyVStack { + ForEach(thread.childEvents, id: \.id) { event in + MutedEventView( + damus_state: damus, + event: event, + scroller: nil, + nav_target: $nav_target, + navigating: $navigating, + selected: false + ) + + Divider() + .padding([.top], 4) + } } }.padding() }.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread.")) diff --git a/damus/Views/Timeline/InnerTimelineView.swift b/damus/Views/Timeline/InnerTimelineView.swift index 3fbf466b..1744a8ca 100644 --- a/damus/Views/Timeline/InnerTimelineView.swift +++ b/damus/Views/Timeline/InnerTimelineView.swift @@ -42,6 +42,9 @@ struct InnerTimelineView: View { navigating = true } .padding(.top, 10) + + Divider() + .padding([.top], 10) } } } diff --git a/damus/Views/Timeline/LoadMoreButton.swift b/damus/Views/Timeline/LoadMoreButton.swift index d98ffdae..cda930e4 100644 --- a/damus/Views/Timeline/LoadMoreButton.swift +++ b/damus/Views/Timeline/LoadMoreButton.swift @@ -23,7 +23,7 @@ struct LoadMoreButton: View { Group { if events.queued > 0 { Button(action: click) { - Text("Load \(events.queued) more") + Text("Load \(events.queued) more", comment: "Button text for loading more events, where the variable is the number of events.") } .font(.system(size: 14, weight: .bold)) .padding(10) diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift index e2a68452..a549ce3d 100644 --- a/damus/Views/TimelineView.swift +++ b/damus/Views/TimelineView.swift @@ -27,17 +27,6 @@ struct TimelineView: View { MainContent } - func handle_scroll(_ proxy: GeometryProxy) { - let offset = -proxy.frame(in: .named("scroll")).origin.y - guard offset >= 0 else { - return - } - let val = offset > 0 - if self.events.should_queue != val { - self.events.should_queue = val - } - } - var realtime_bar_opacity: Double { colorScheme == .dark ? 0.2 : 0.1 } @@ -55,7 +44,7 @@ struct TimelineView: View { .disabled(loading) .background(GeometryReader { proxy -> Color in DispatchQueue.main.async { - handle_scroll(proxy) + handle_scroll_queue(proxy, queue: self.events) } return Color.clear }) @@ -82,3 +71,18 @@ struct TimelineView_Previews: PreviewProvider { } +protocol ScrollQueue { + var should_queue: Bool { get } + func set_should_queue(_ val: Bool) +} + +func handle_scroll_queue(_ proxy: GeometryProxy, queue: ScrollQueue) { + let offset = -proxy.frame(in: .named("scroll")).origin.y + guard offset >= 0 else { + return + } + let val = offset > 0 + if queue.should_queue != val { + queue.set_should_queue(val) + } +} diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift new file mode 100644 index 00000000..613ad3ee --- /dev/null +++ b/damus/Views/Zaps/CustomizeZapView.swift @@ -0,0 +1,215 @@ +// +// CustomizeZapView.swift +// damus +// +// Created by William Casarin on 2023-02-25. +// + +import SwiftUI +import Combine + +enum ZapType { + case pub + case anon + case non_zap +} + +struct ZapAmountItem: Identifiable, Hashable { + let amount: Int + let icon: String + + var id: String { + return icon + } +} + +func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem { + let def = get_default_zap_amount(pubkey: pubkey) ?? 1000 + return ZapAmountItem(amount: def, icon: "🤙") +} + +func get_zap_amount_items(pubkey: String) -> [ZapAmountItem] { + let def_item = get_default_zap_amount_item(pubkey) + var entries = [ + ZapAmountItem(amount: 500, icon: "🙂"), + ZapAmountItem(amount: 5000, icon: "💜"), + ZapAmountItem(amount: 10_000, icon: "😍"), + ZapAmountItem(amount: 20_000, icon: "🤩"), + ZapAmountItem(amount: 50_000, icon: "🔥"), + ZapAmountItem(amount: 100_000, icon: "🚀"), + ZapAmountItem(amount: 1_000_000, icon: "🤯"), + ] + entries.append(def_item) + + entries.sort { $0.amount < $1.amount } + return entries +} + +struct CustomizeZapView: View { + let state: DamusState + let event: NostrEvent + let lnurl: String + @State var comment: String + @State var custom_amount: String + @State var custom_amount_sats: Int? + @State var selected_amount: ZapAmountItem + @State var zap_type: ZapType + @State var invoice: String + @State var error: String? + @State var showing_wallet_selector: Bool + @State var zapping: Bool + + let zap_amounts: [ZapAmountItem] + + @Environment(\.dismiss) var dismiss + + init(state: DamusState, event: NostrEvent, lnurl: String) { + self._comment = State(initialValue: "") + self.event = event + self.zap_amounts = get_zap_amount_items(pubkey: state.pubkey) + self._error = State(initialValue: nil) + self._invoice = State(initialValue: "") + self._showing_wallet_selector = State(initialValue: false) + self._custom_amount = State(initialValue: "") + self._zap_type = State(initialValue: .pub) + let selected = get_default_zap_amount_item(state.pubkey) + self._selected_amount = State(initialValue: selected) + self._custom_amount_sats = State(initialValue: nil) + self._zapping = State(initialValue: false) + self.lnurl = lnurl + self.state = state + } + + var ZapTypePicker: some View { + Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) { + Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub) + Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon) + Text("Non-Zap", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap) + } + .pickerStyle(.segmented) + } + + var AmountPicker: some View { + Picker(NSLocalizedString("Zap Amount", comment: "Title of picker that allows selection of predefined amounts to zap."), selection: $selected_amount) { + ForEach(zap_amounts) { entry in + let fmt = format_msats_abbrev(Int64(entry.amount) * 1000) + HStack(alignment: .firstTextBaseline) { + Text("\(entry.icon)") + .frame(width: 30) + Text("\(fmt)") + .frame(width: 50) + } + .tag(entry) + } + } + .pickerStyle(.wheel) + } + + func receive_zap(notif: Notification) { + let zap_ev = notif.object as! ZappingEvent + guard zap_ev.is_custom else { + return + } + guard zap_ev.event.id == event.id else { + return + } + + self.zapping = false + + switch zap_ev.type { + case .failed(let err): + switch err { + case .fetching_invoice: + self.error = "Error fetching lightning invoice" + case .bad_lnurl: + self.error = "Invalid lightning address" + } + break + case .got_zap_invoice(let inv): + if should_show_wallet_selector(state.pubkey) { + self.invoice = inv + self.showing_wallet_selector = true + } else { + open_with_wallet(wallet: get_default_wallet(state.pubkey).model, invoice: inv) + self.showing_wallet_selector = false + dismiss() + } + } + + +} + + var body: some View { + MainContent + .sheet(isPresented: $showing_wallet_selector) { + SelectWalletView(showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice) + } + .onReceive(handle_notify(.zapping)) { notif in + receive_zap(notif: notif) + } + .ignoresSafeArea() + } + + var MainContent: some View { + VStack(alignment: .leading) { + Form { + Section(content: { + AmountPicker + }, header: { + Text("Zap Amount in sats", comment: "Header text to indicate that the picker below it is to choose a pre-defined amount of sats to zap.") + }) + + Section(content: { + TextField(String("100000"), text: $custom_amount) + .keyboardType(.numberPad) + .onReceive(Just(custom_amount)) { newValue in + + if let parsed = handle_string_amount(new_value: newValue) { + self.custom_amount = String(parsed) + self.custom_amount_sats = parsed + } + } + }, header: { + Text("Custom Zap Amount", comment: "Header text to indicate that the text field below it is to enter a custom zap amount.") + }) + .dismissKeyboardOnTap() + + Section(content: { + TextField(NSLocalizedString("Awesome post!", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment) + }, header: { + Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.") + }) + .dismissKeyboardOnTap() + + Section(content: { + ZapTypePicker + }, header: { + Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.") + }) + + if zapping { + Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.") + } else { + Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) { + let amount = custom_amount_sats ?? selected_amount.amount + send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type) + self.zapping = true + } + .zIndex(16) + } + + if let error { + Text(error) + .foregroundColor(.red) + } + } + } + } +} + +struct CustomizeZapView_Previews: PreviewProvider { + static var previews: some View { + CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "") + .frame(width: 400, height: 600) + } +} diff --git a/damus/ar.lproj/InfoPlist.strings b/damus/ar.lproj/InfoPlist.strings index 8e309fb5..3841fdae 100644 Binary files a/damus/ar.lproj/InfoPlist.strings and b/damus/ar.lproj/InfoPlist.strings differ diff --git a/damus/ar.lproj/Localizable.strings b/damus/ar.lproj/Localizable.strings index 5939d083..215f103f 100644 Binary files a/damus/ar.lproj/Localizable.strings and b/damus/ar.lproj/Localizable.strings differ diff --git a/damus/ar.lproj/Localizable.stringsdict b/damus/ar.lproj/Localizable.stringsdict index 6f616cae..d2bba902 100644 --- a/damus/ar.lproj/Localizable.stringsdict +++ b/damus/ar.lproj/Localizable.stringsdict @@ -4,51 +4,51 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - %d منشورات اضافية - many - %d منشورات اضافية - one - %d منشور اضافي - other - %d منشورات اضافية - two - %d منشوران zero - %d منشورات أخرى + ... %d منشورات أخرى ... + one + ... %d منشور اضافي ... + two + ... %d منشوران ... + few + ... %d منشورات اضافية ... + many + ... %d منشورات اضافية ... + other + ... %d منشورات اضافية ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - المتابعون - many - المتابعون - one - متابع - other - المتابعون - two - متابعان zero متابع + one + متابع + two + متابعان + few + المتابِعون + many + المتابِعون + other + المتابِعون - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -60,18 +60,18 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + تفاعل + one + تفاعل + two + تفاعلان few تفاعلات many تفاعل - one - تفاعل other تفاعل - two - تفاعل - zero - تفاعل relays_count @@ -84,66 +84,66 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + موصّل + one + موصّل + two + موصّلان few موصّلات many - موصّلات - one - موصّل + موصّل other - موصّلات - two - موصّلان - zero موصّل replying_to_one_and_others NSStringLocalizedFormatKey - رد على %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d آخرون - many - & %d آخرون - one - & %d آخر - other - & %d آخرين - two - & %d آخران zero - + رد على %2$@ + one + الرد على %2$@ & %1$d آخر + two + الرد على %2$@ & %1$d آخرين + few + الرد على %2$@ & %1$d آخرين + many + الرد على %2$@ & %1$d آخرين + other + الرد على %2$@ & %1$d آخرين replying_to_two_and_others NSStringLocalizedFormatKey - رد على%@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d آخرون - many - & %d آخرون - one - & %d آخر - other - & %d آخرين - two - & %d آخران zero - + الرد على %2$@, %3$@ & %1$d others + one + الرد على %2$@, %3$@ & %1$d آخر + two + الرد على %2$@, %3$@ & %1$d آخرين + few + الرد على %2$@, %3$@ & %1$d آخرين + many + الرد على %2$@, %3$@ & %1$d آخرين + other + الرد على %2$@, %3$@ & %1$d آخرين reposts_count @@ -156,18 +156,18 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - اعادات نشر - many + zero اعادات نشر one اعادة نشر - other - اعادات نشر two + اعادتا نشر + few اعادات نشر - zero - اعادات نشر + many + اعادة نشر + other + اعادة نشر sats_count @@ -180,18 +180,18 @@ NSStringPluralRuleType NSStringFormatValueTypeKey @ + zero + %2$@ ساتوشي + one + %2$@ ساتوشي + two + %2$@ ساتوشي few %2$@ ساتوشي many %2$@ ساتوشي - one - %2$@ ساتوشي other %2$@ ساتوشي - two - %2$@ ساتوشي - zero - %2$@ ساتوشي zaps_count @@ -204,18 +204,18 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - وميض - many + zero وميض one ومضة - other - وميض two - وميض - zero - وميض + ومضتان + few + ومضات + many + ومضة + other + ومضة diff --git a/damus/cs.lproj/InfoPlist.strings b/damus/cs.lproj/InfoPlist.strings index 935ad617..192eda4c 100644 Binary files a/damus/cs.lproj/InfoPlist.strings and b/damus/cs.lproj/InfoPlist.strings differ diff --git a/damus/cs.lproj/Localizable.strings b/damus/cs.lproj/Localizable.strings index 730fa87c..8e8d13af 100644 Binary files a/damus/cs.lproj/Localizable.strings and b/damus/cs.lproj/Localizable.strings differ diff --git a/damus/cs.lproj/Localizable.stringsdict b/damus/cs.lproj/Localizable.stringsdict index ff34d940..5711b13a 100644 --- a/damus/cs.lproj/Localizable.stringsdict +++ b/damus/cs.lproj/Localizable.stringsdict @@ -4,43 +4,43 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - %d other notes - many - %d other notes one - %d jiná poznámka + ... %d jiná poznámka ... + few + ... %d other notes ... + many + ... %d other notes ... other - %d jiné poznámky + ... %d jiné poznámky ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Seguidor few Followers many Followers - one - Seguidor other Sledují - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -52,12 +52,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Reakce few Reactions many Reactions - one - Reakce other Reakce @@ -72,12 +72,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Relé few Relays many Relays - one - Relé other Relé @@ -85,45 +85,41 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Odpověď na %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - a %d další + Odpověď na %2$@ a %1$d další + few + Odpověď na %2$@ a %1$d others + many + Odpověď na %2$@ a %1$d others other - a %d další - zero - + Odpověď na %2$@ a %1$d další replying_to_two_and_others NSStringLocalizedFormatKey - Odpovědět na %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - a %d další + Odpovědět na %2$@, %3$@ & %1$d další + few + Odpovědět na %2$@, %3$@ & %1$d others + many + Odpovědět na %2$@, %3$@ & %1$d others other - a %d další - zero - + Odpovědět na %2$@, %3$@ & %1$d další reposts_count @@ -136,12 +132,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Přesdílet few Reposts many Reposts - one - Přesdílet other Přesdílené @@ -156,12 +152,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey @ + one + %2$@ sat few %2$@ sats many %2$@ sats - one - %2$@ sat other %2$@ satů @@ -176,12 +172,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Zap few Zaps many Zaps - one - Zap other Zapů diff --git a/damus/de.lproj/InfoPlist.strings b/damus/de.lproj/InfoPlist.strings index 9bff2e1a..ab356d91 100644 Binary files a/damus/de.lproj/InfoPlist.strings and b/damus/de.lproj/InfoPlist.strings differ diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings index 68a8ec24..f7a83a8a 100644 Binary files a/damus/de.lproj/Localizable.strings and b/damus/de.lproj/Localizable.strings differ diff --git a/damus/de.lproj/Localizable.stringsdict b/damus/de.lproj/Localizable.stringsdict index 8bede21a..0cf15cc3 100644 --- a/damus/de.lproj/Localizable.stringsdict +++ b/damus/de.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,15 @@ NSStringFormatValueTypeKey d one - %d andere Notiz + ... %d andere Notiz ... other - %d andere Notizen + ... %d andere Notizen ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -31,8 +33,6 @@ other Follower - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -69,7 +69,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Antwort an %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +77,15 @@ NSStringFormatValueTypeKey d one - & %d andere + Antwort an %2$@ & %1$d andere other - & %d andere - zero - + Antwort an %2$@ & %1$d andere replying_to_two_and_others NSStringLocalizedFormatKey - Antwort an %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +93,9 @@ NSStringFormatValueTypeKey d one - & %d andere + Antwort an %2$@, %3$@ & %1$d andere other - & %d andere - zero - + Antwort an %2$@, %3$@ & %1$d andere reposts_count diff --git a/damus/el-GR.lproj/InfoPlist.strings b/damus/el-GR.lproj/InfoPlist.strings index 59d7f996..91d408c9 100644 Binary files a/damus/el-GR.lproj/InfoPlist.strings and b/damus/el-GR.lproj/InfoPlist.strings differ diff --git a/damus/el-GR.lproj/Localizable.strings b/damus/el-GR.lproj/Localizable.strings index 19ee5ec0..df41af5f 100644 Binary files a/damus/el-GR.lproj/Localizable.strings and b/damus/el-GR.lproj/Localizable.strings differ diff --git a/damus/el-GR.lproj/Localizable.stringsdict b/damus/el-GR.lproj/Localizable.stringsdict index dab971b4..41ced9e5 100644 --- a/damus/el-GR.lproj/Localizable.stringsdict +++ b/damus/el-GR.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,15 @@ NSStringFormatValueTypeKey d one - %d άλλη σημείωση + ... %d άλλη σημείωση ... other - %d άλλες σημειώσεις + ... %d άλλες σημειώσεις ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -31,8 +33,6 @@ other Ακόλουθοι - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -69,7 +69,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Απάντηση προς %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +77,15 @@ NSStringFormatValueTypeKey d one - & %d άλλον + Replying to %2$@ & %1$d other other - & %d άλλους - zero - + Replying to %2$@ & %1$d others replying_to_two_and_others NSStringLocalizedFormatKey - Απάντηση προς %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +93,9 @@ NSStringFormatValueTypeKey d one - & %d άλλον + Απάντηση προς %2$@, %3$@ & %1$d άλλον other - & %d άλλους - zero - + Απάντηση προς %2$@, %3$@ & %1$d άλλους reposts_count diff --git a/damus/en-US.lproj/Localizable.strings b/damus/en-US.lproj/Localizable.strings new file mode 100644 index 00000000..17fd6e3e Binary files /dev/null and b/damus/en-US.lproj/Localizable.strings differ diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict index 4441ecbf..53fbef3a 100644 --- a/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.lproj/Localizable.stringsdict @@ -34,6 +34,54 @@ Followers + reacted_tagged_in_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to a post you were tagged in + other + %2$@ and %1$d others reacted to a post you were tagged in + + + reacted_your_post_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to your post + other + %2$@ and %1$d others reacted to your post + + + reacted_your_profile_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to your profile + other + %2$@ and %1$d others reacted to your profile + + reactions_count NSStringLocalizedFormatKey @@ -66,22 +114,6 @@ Relays - replying_to_one_and_others - - NSStringLocalizedFormatKey - %#@OTHERS@ - OTHERS - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - d - one - Replying to %2$@ & %1$d other - other - Replying to %2$@ & %1$d others - - replying_to_two_and_others NSStringLocalizedFormatKey @@ -98,6 +130,54 @@ Replying to %2$@, %3$@ & %1$d others + reposted_tagged_in_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted a post you were tagged in + other + %2$@ and %1$d others reposted a post you were tagged in + + + reposted_your_post_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted your post + other + %2$@ and %1$d others reposted your post + + + reposted_your_profile_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted your profile + other + %2$@ and %1$d others reposted your profile + + reposts_count NSStringLocalizedFormatKey @@ -130,6 +210,54 @@ %2$@ sats + zapped_tagged_in_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped a post you were tagged in + other + %2$@ and %1$d others zapped a post you were tagged in + + + zapped_your_post_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped your post + other + %2$@ and %1$d others zapped your post + + + zapped_your_profile_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped your profile + other + %2$@ and %1$d others zapped your profile + + zaps_count NSStringLocalizedFormatKey diff --git a/damus/en-US.xcloc/Localized Contents/en-US.xliff b/damus/en-US.xcloc/Localized Contents/en-US.xliff index 778e5c73..ba87f69d 100644 --- a/damus/en-US.xcloc/Localized Contents/en-US.xliff +++ b/damus/en-US.xcloc/Localized Contents/en-US.xliff @@ -32,6 +32,11 @@ + + %@ + %@ + No comment provided by engineer. + %@ %@ %@ %@ @@ -58,6 +63,11 @@ Sentence composed of 2 variables to describe how many people are following a use %@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet. Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string. + + %lld + %lld + No comment provided by engineer. + %lld/%lld %lld/%lld @@ -73,15 +83,10 @@ Sentence composed of 2 variables to describe how many people are following a use '%@' is an invalid NIP-05 identifier. It should look like an email. Description of why the nip05 identifier is invalid. - - (Profile.displayName(profile: profile, pubkey: whos))'s Followers - (Profile.displayName(profile: profile, pubkey: whos))'s Followers - Navigation bar title for view that shows who is following a user. - - - (who) following - (who) following - Navigation bar title for view that shows who a user is following. + + ?? + ?? + No comment provided by engineer. API Key (optional) @@ -129,6 +134,11 @@ Sentence composed of 2 variables to describe how many people are following a use Button to add recommended relay server. Button to confirm adding user inputted relay. + + Add Bookmark + Add Bookmark + Context menu option for adding a note bookmark. + Add Relay Add Relay @@ -144,6 +154,11 @@ Sentence composed of 2 variables to describe how many people are following a use Admin Label to display relay contact user. + + Anonymous + Anonymous + Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it. + Any Any @@ -154,6 +169,11 @@ Sentence composed of 2 variables to describe how many people are following a use Are you sure you want to repost this? Alert message to ask if user wants to repost a post. + + Awesome post! + Awesome post! + Placeholder text for a comment to send as part of a zap to the user. + + + Bookmarks + Bookmarks + Sidebar menu label for Bookmarks view. + Title of bookmarks view + Boosts Boosts @@ -247,11 +273,21 @@ Sentence composed of 2 variables to describe how many people are following a use Clear Button for clearing cached data. + + Clear All + Clear All + Button for clearing bookmarks data. + Clear Cache Clear Cache Section title for clearing cached data. + + Comment + Comment + Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user. + Contact Contact @@ -348,6 +384,11 @@ Sentence composed of 2 variables to describe how many people are following a use Custom Dropdown option for selecting a custom translation server. + + Custom Zap Amount + Custom Zap Amount + Header text to indicate that the text field below it is to enter a custom zap amount. + DMs DMs @@ -470,12 +511,12 @@ Sentence composed of 2 variables to describe how many people are following a use Followers Followers - Label describing followers of a user. + Navigation bar title for view that shows who is following a user. Following Following - Part of a larger sentence to describe how many profiles a user is following. + Navigation bar title for view that shows who a user is following. Following... @@ -573,6 +614,11 @@ Sentence composed of 2 variables to describe how many people are following a use Like Accessibility Label for Like button + + Load %lld more + Load %lld more + Button text for loading more events, where the variable is the number of events. + Local authentication to access private key Local authentication to access private key @@ -621,6 +667,11 @@ Sentence composed of 2 variables to describe how many people are following a use No block list found, create a new one? This will overwrite any previous block lists. Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. + + Non-Zap + Non-Zap + Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap. + None None @@ -676,6 +727,11 @@ Sentence composed of 2 variables to describe how many people are following a use Plan Prompt selection of DeepL subscription plan to perform machine translations on notes + + Please choose relays from the list below to filter the current feed: + Please choose relays from the list below to filter the current feed: + Instructions on how to filter a specific timeline feed by choosing relay servers to filter on. + Post Post @@ -723,6 +779,11 @@ Label for filter for seeing your posts and replies (instead of only your posts). Profile Picture Label for Profile Picture section of user profile form. + + Public + Public + Picker option to indicate that a zap should be sent publicly and identify the user as who sent it. + Public Account ID Public Account ID @@ -774,6 +835,11 @@ Label for filter for seeing your posts and replies (instead of only your posts). Relays have been notified and clients will be able to use this information to filter content. Thank you! Description of what was done as a result of sending a report to relay servers. + + Remove Bookmark + Remove Bookmark + Context menu option for removing a note bookmark. + Remove all Remove all @@ -986,9 +1052,9 @@ Label for filter for seeing your posts and replies (instead of only your posts). This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key. Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. - - This is your account ID, you can give this to your friends so that they can follow you. Click to copy. - This is your account ID, you can give this to your friends so that they can follow you. Click to copy. + + This is your account ID, you can give this to your friends so that they can follow you. Tap to copy. + This is your account ID, you can give this to your friends so that they can follow you. Tap to copy. Label to describe that a public key is the user's account ID and what they can do with it. @@ -1001,11 +1067,6 @@ Label for filter for seeing your posts and replies (instead of only your posts). Thread Navigation bar title for note thread. - - To filter your %@ feed, please choose applicable relays from the list below: - To filter your %@ feed, please choose applicable relays from the list below: - Instructions on how to filter a specific timeline feed by choosing relay servers to filter on. - Translate Note Translate Note @@ -1123,6 +1184,11 @@ Label for filter for seeing your posts and replies (instead of only your posts). Yes, Post with Private Key Button to proceed with posting a note even though it looks like they might be posting a private key. + + You have no bookmarks yet, add them in the context menu + You have no bookmarks yet, add them in the context menu + Text indicating that there are no bookmarks to be viewed + Your Name Your Name @@ -1136,7 +1202,28 @@ Label for filter for seeing your posts and replies (instead of only your posts). Zap Zap - Accessibility label for zap button + Accessibility label for zap button + Button to send a zap. + + + Zap Amount + Zap Amount + Title of picker that allows selection of predefined amounts to zap. + + + Zap Amount in sats + Zap Amount in sats + Header text to indicate that the picker below it is to choose a pre-defined amount of sats to zap. + + + Zap Type + Zap Type + Header text to indicate that the picker below it is to choose the type of zap to send. + + + Zapping... + Zapping... + Text to indicate that the app is in the process of sending a zap. Zaps @@ -1188,6 +1275,66 @@ Label for filter for seeing your posts and replies (instead of only your posts). optional Label indicating that a form input is optional. + + %@ reacted to a post you were tagged in + %@ reacted to a post you were tagged in + Notification that a user reacted to a post that the current user was tagged in + + + %@ and %@ reacted to a post you were tagged in + %@ and %@ reacted to a post you were tagged in + Notification that 2 users reacted to a post that the current user was tagged in + + + %@ reacted to your post + %@ reacted to your post + Notification that a user reacted to the current user's post + + + %@ and %@ reacted to your post + %@ and %@ reacted to your post + Notification that 2 users reacted to the current user's profile + + + %@ reacted to your profile + %@ reacted to your profile + Notification that a user reacted to the current user's profile + + + %@ and %@ reacted to your profile + %@ and %@ reacted to your profile + Notification that 2 users reacted to the current user's profile + + + %@ reposted a post you were tagged in + %@ reposted a post you were tagged in + Notification that a user reposted a post that the current user was tagged in + + + %@ and %@ reposted a post you were tagged in + %@ and %@ reposted a post you were tagged in + Notification that 2 users reposted a post that the current user was tagged in + + + %@ reposted your post + %@ reposted your post + Notification that a user reposted the current user's post + + + %@ and %@ reposted your post + %@ and %@ reposted your post + Notification that 2 users reposted the current user's post + + + %@ reposted your profile + %@ reposted your profile + Notification that a user reposted the current user's profile + + + %@ and %@ reposted your profile + %@ and %@ reposted your profile + Notification that 2 users reposted the current user's profile + satoshi satoshi @@ -1203,6 +1350,36 @@ Label for filter for seeing your posts and replies (instead of only your posts). you 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. + + %@ zapped a post you were tagged in + %@ zapped a post you were tagged in + Notification that a user zapped a post that the current user was tagged in + + + %@ and %@ zapped a post you were tagged in + %@ and %@ zapped a post you were tagged in + Notification that 2 users zapped a post that the current user was tagged in + + + %@ zapped your post + %@ zapped your post + Notification that a user zapped the current user's post + + + %@ and %@ zapped your post + %@ and %@ zapped your post + Notification that 2 users zapped the current user's post + + + %@ zapped your profile + %@ zapped your profile + Notification that a user zapped the current user's profile + + + %@ and %@ zapped your profile + %@ and %@ zapped your profile + Notification that 2 users zapped the current user's profile + ⚡️ %@ ⚡️ %@ @@ -1245,6 +1422,51 @@ Label for filter for seeing your posts and replies (instead of only your posts). %#@FOLLOWERS@ + + %#@REACTED@ + %#@REACTED@ + + + + %2$@ and %1$d other reacted to a post you were tagged in + %2$@ and %1$d other reacted to a post you were tagged in + + + + %2$@ and %1$d others reacted to a post you were tagged in + %2$@ and %1$d others reacted to a post you were tagged in + + + + %#@REACTED@ + %#@REACTED@ + + + + %2$@ and %1$d other reacted to your post + %2$@ and %1$d other reacted to your post + + + + %2$@ and %1$d others reacted to your post + %2$@ and %1$d others reacted to your post + + + + %#@REACTED@ + %#@REACTED@ + + + + %2$@ and %1$d other reacted to your profile + %2$@ and %1$d other reacted to your profile + + + + %2$@ and %1$d others reacted to your profile + %2$@ and %1$d others reacted to your profile + + %#@REACTIONS@ %#@REACTIONS@ @@ -1275,21 +1497,6 @@ Label for filter for seeing your posts and replies (instead of only your posts). Relays - - %#@OTHERS@ - %#@OTHERS@ - - - - Replying to %2$@ & %1$d other - Replying to %2$@ & %1$d other - - - - Replying to %2$@ & %1$d others - Replying to %2$@ & %1$d others - - %#@OTHERS@ %#@OTHERS@ @@ -1305,6 +1512,51 @@ Label for filter for seeing your posts and replies (instead of only your posts). Replying to %2$@, %3$@ & %1$d others + + %#@REPOSTED@ + %#@REPOSTED@ + + + + %2$@ and %1$d other reposted a post you were tagged in + %2$@ and %1$d other reposted a post you were tagged in + + + + %2$@ and %1$d others reposted a post you were tagged in + %2$@ and %1$d others reposted a post you were tagged in + + + + %#@REPOSTED@ + %#@REPOSTED@ + + + + %2$@ and %1$d other reposted your post + %2$@ and %1$d other reposted your post + + + + %2$@ and %1$d others reposted your post + %2$@ and %1$d others reposted your post + + + + %#@REPOSTED@ + %#@REPOSTED@ + + + + %2$@ and %1$d other reposted your profile + %2$@ and %1$d other reposted your profile + + + + %2$@ and %1$d others reposted your profile + %2$@ and %1$d others reposted your profile + + %#@REPOSTS@ %#@REPOSTS@ @@ -1335,6 +1587,51 @@ Label for filter for seeing your posts and replies (instead of only your posts). %2$@ sats + + %#@ZAPPED@ + %#@ZAPPED@ + + + + %2$@ and %1$d other zapped a post you were tagged in + %2$@ and %1$d other zapped a post you were tagged in + + + + %2$@ and %1$d others zapped a post you were tagged in + %2$@ and %1$d others zapped a post you were tagged in + + + + %#@ZAPPED@ + %#@ZAPPED@ + + + + %2$@ and %1$d other zapped your post + %2$@ and %1$d other zapped your post + + + + %2$@ and %1$d others zapped your post + %2$@ and %1$d others zapped your post + + + + %#@ZAPPED@ + %#@ZAPPED@ + + + + %2$@ and %1$d other zapped your profile + %2$@ and %1$d other zapped your profile + + + + %2$@ and %1$d others zapped your profile + %2$@ and %1$d others zapped your profile + + %#@ZAPS@ %#@ZAPS@ 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 index ee39b694..10543b39 100644 Binary files a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings and b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings differ diff --git a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict index 4441ecbf..53fbef3a 100644 --- a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict @@ -34,6 +34,54 @@ Followers + reacted_tagged_in_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to a post you were tagged in + other + %2$@ and %1$d others reacted to a post you were tagged in + + + reacted_your_post_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to your post + other + %2$@ and %1$d others reacted to your post + + + reacted_your_profile_3 + + NSStringLocalizedFormatKey + %#@REACTED@ + REACTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reacted to your profile + other + %2$@ and %1$d others reacted to your profile + + reactions_count NSStringLocalizedFormatKey @@ -66,22 +114,6 @@ Relays - replying_to_one_and_others - - NSStringLocalizedFormatKey - %#@OTHERS@ - OTHERS - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - d - one - Replying to %2$@ & %1$d other - other - Replying to %2$@ & %1$d others - - replying_to_two_and_others NSStringLocalizedFormatKey @@ -98,6 +130,54 @@ Replying to %2$@, %3$@ & %1$d others + reposted_tagged_in_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted a post you were tagged in + other + %2$@ and %1$d others reposted a post you were tagged in + + + reposted_your_post_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted your post + other + %2$@ and %1$d others reposted your post + + + reposted_your_profile_3 + + NSStringLocalizedFormatKey + %#@REPOSTED@ + REPOSTED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other reposted your profile + other + %2$@ and %1$d others reposted your profile + + reposts_count NSStringLocalizedFormatKey @@ -130,6 +210,54 @@ %2$@ sats + zapped_tagged_in_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped a post you were tagged in + other + %2$@ and %1$d others zapped a post you were tagged in + + + zapped_your_post_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped your post + other + %2$@ and %1$d others zapped your post + + + zapped_your_profile_3 + + NSStringLocalizedFormatKey + %#@ZAPPED@ + ZAPPED + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %2$@ and %1$d other zapped your profile + other + %2$@ and %1$d others zapped your profile + + zaps_count NSStringLocalizedFormatKey diff --git a/damus/es-419.lproj/InfoPlist.strings b/damus/es-419.lproj/InfoPlist.strings index cb67cc98..8e2d9242 100644 Binary files a/damus/es-419.lproj/InfoPlist.strings and b/damus/es-419.lproj/InfoPlist.strings differ diff --git a/damus/es-419.lproj/Localizable.strings b/damus/es-419.lproj/Localizable.strings index a0305a31..e426441d 100644 Binary files a/damus/es-419.lproj/Localizable.strings and b/damus/es-419.lproj/Localizable.strings differ diff --git a/damus/es-419.lproj/Localizable.stringsdict b/damus/es-419.lproj/Localizable.stringsdict index 740d250b..6fce4a53 100644 --- a/damus/es-419.lproj/Localizable.stringsdict +++ b/damus/es-419.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,17 @@ NSStringFormatValueTypeKey d one - %d otra nota + ... %d otra nota ... + many + ... %d otras notas ... other - %d otras notas + ... %d otras notas... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -28,11 +32,11 @@ d one Seguidor + many + Seguidores other Seguidores - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -46,6 +50,8 @@ d one Reacción + many + Reacciones other Reacciones @@ -62,6 +68,8 @@ d one Relé + many + Relés other Relés @@ -69,7 +77,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Respondiendo a %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +85,17 @@ NSStringFormatValueTypeKey d one - y %d otro + Respondiendo a %2$@ y %1$d otro + many + Respondiendo a %2$@ y %1$d otros other - y %d otros - zero - + Respondiendo a %2$@ y %1$d otros replying_to_two_and_others NSStringLocalizedFormatKey - Respondiendo a %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +103,11 @@ NSStringFormatValueTypeKey d one - y %d otro + Respondiendo a %2$@, %3$@ y %1$d otro + many + Respondiendo a %2$@, %3$@ y %1$d otros other - y %d otros - zero - + Respondiendo a %2$@, %3$@ y %1$d otros reposts_count @@ -114,6 +122,8 @@ d one Republicación + many + Republicaciones other Republicaciones @@ -130,6 +140,8 @@ @ one %2$@ sat + many + %2$@ sats other %2$@ sats @@ -146,6 +158,8 @@ d one Zap + many + Zaps other Zaps diff --git a/damus/fa.lproj/InfoPlist.strings b/damus/fa.lproj/InfoPlist.strings new file mode 100644 index 00000000..f153d01c Binary files /dev/null and b/damus/fa.lproj/InfoPlist.strings differ diff --git a/damus/fa.lproj/Localizable.strings b/damus/fa.lproj/Localizable.strings new file mode 100644 index 00000000..1eada13b Binary files /dev/null and b/damus/fa.lproj/Localizable.strings differ diff --git a/damus/fr-FR.lproj/InfoPlist.strings b/damus/fr-FR.lproj/InfoPlist.strings index 5e1126a0..14b5b6ac 100644 Binary files a/damus/fr-FR.lproj/InfoPlist.strings and b/damus/fr-FR.lproj/InfoPlist.strings differ diff --git a/damus/fr-FR.lproj/Localizable.strings b/damus/fr-FR.lproj/Localizable.strings index f55cf525..12929963 100644 Binary files a/damus/fr-FR.lproj/Localizable.strings and b/damus/fr-FR.lproj/Localizable.strings differ diff --git a/damus/fr-FR.lproj/Localizable.stringsdict b/damus/fr-FR.lproj/Localizable.stringsdict index 5d27f603..84f37575 100644 --- a/damus/fr-FR.lproj/Localizable.stringsdict +++ b/damus/fr-FR.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,17 @@ NSStringFormatValueTypeKey d one - %d autre note + ... %d autre note ... + many + ... %d autres notes ... other - %d autres notes + ... %d autres notes ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -28,11 +32,11 @@ d one Abonné + many + Abonnés other Abonnés - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -46,6 +50,8 @@ d one Réaction + many + Réactions other Réactions @@ -62,6 +68,8 @@ d one Relais + many + Relais other Relais @@ -69,7 +77,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Réponse à %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +85,17 @@ NSStringFormatValueTypeKey d one - & %d autre + Réponse à %2$@ & %1$d autre + many + Réponse à %2$@ & %1$d autres other - & %d autres - zero - + Réponse à %2$@ & %1$d autres replying_to_two_and_others NSStringLocalizedFormatKey - Réponse à %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +103,11 @@ NSStringFormatValueTypeKey d one - & %d autre + Réponse à %2$@, %3$@ & %1$d autre + many + Réponse à %2$@, %3$@ & %1$d autres other - & %d autres - zero - + Réponse à %2$@, %3$@ & %1$d autres reposts_count @@ -114,6 +122,8 @@ d one Republication + many + Republications other Republications @@ -130,6 +140,8 @@ @ one %2$@ sat + many + %2$@ sats other %2$@ sats @@ -146,6 +158,8 @@ d one Zap + many + Zaps other Zaps diff --git a/damus/id.lproj/InfoPlist.strings b/damus/id.lproj/InfoPlist.strings index 4ed3cb82..7612949e 100644 Binary files a/damus/id.lproj/InfoPlist.strings and b/damus/id.lproj/InfoPlist.strings differ diff --git a/damus/id.lproj/Localizable.stringsdict b/damus/id.lproj/Localizable.stringsdict index 05594248..e50aa9f4 100644 --- a/damus/id.lproj/Localizable.stringsdict +++ b/damus/id.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,13 +13,13 @@ NSStringFormatValueTypeKey d other - %d Note Lainnya + ... %d Note Lainnya ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -27,8 +29,6 @@ other Pengikut - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -61,7 +61,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Membalas ke %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -69,15 +69,13 @@ NSStringFormatValueTypeKey d other - & %d lainnya - zero - + Membalas ke %2$@ & %1$d lainnya replying_to_two_and_others NSStringLocalizedFormatKey - Membalas ke %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -85,9 +83,7 @@ NSStringFormatValueTypeKey d other - & %d lainnya - zero - + Membalas ke %2$@, %3$@ & %1$d lainnya reposts_count diff --git a/damus/it-IT.lproj/InfoPlist.strings b/damus/it-IT.lproj/InfoPlist.strings index a1b52641..182f62ae 100644 Binary files a/damus/it-IT.lproj/InfoPlist.strings and b/damus/it-IT.lproj/InfoPlist.strings differ diff --git a/damus/it-IT.lproj/Localizable.stringsdict b/damus/it-IT.lproj/Localizable.stringsdict index d728d3cc..265a6547 100644 --- a/damus/it-IT.lproj/Localizable.stringsdict +++ b/damus/it-IT.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,17 @@ NSStringFormatValueTypeKey d one - %d altra nota + ... %d altra nota ... + many + ... %d altre note ... other - %d altre note + ... %d altre note ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -28,11 +32,11 @@ d one Seguace + many + Seguaci other Seguaci - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -46,6 +50,8 @@ d one Reazione + many + Reazioni other Reazioni @@ -62,6 +68,8 @@ d one Relè + many + Relè other Relè @@ -69,7 +77,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Rispondendo a %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +85,17 @@ NSStringFormatValueTypeKey d one - & %d altro + Rispondendo a %2$@ & %1$d altro + many + Rispondendo a %2$@ & %1$d altri other - & %d altri - zero - + Rispondendo a %2$@ & %1$d altri replying_to_two_and_others NSStringLocalizedFormatKey - Rispondendo a %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +103,11 @@ NSStringFormatValueTypeKey d one - & %d altro + Rispondendo a %2$@, %3$@ & %1$d altro + many + Rispondendo a %2$@, %3$@ & %1$d altri other - & %d altri - zero - + Rispondendo a %2$@, %3$@ & %1$d altri reposts_count @@ -114,6 +122,8 @@ d one Repost + many + I Repost other I Repost @@ -130,6 +140,8 @@ @ one %2$@ sat + many + %2$@ sats other %2$@ sats @@ -146,6 +158,8 @@ d one Zap + many + Zaps other Zaps diff --git a/damus/ja.lproj/InfoPlist.strings b/damus/ja.lproj/InfoPlist.strings index f618c36e..59f25bd1 100644 Binary files a/damus/ja.lproj/InfoPlist.strings and b/damus/ja.lproj/InfoPlist.strings differ diff --git a/damus/ja.lproj/Localizable.strings b/damus/ja.lproj/Localizable.strings index 6c8d32d1..f2f066d3 100644 Binary files a/damus/ja.lproj/Localizable.strings and b/damus/ja.lproj/Localizable.strings differ diff --git a/damus/ja.lproj/Localizable.stringsdict b/damus/ja.lproj/Localizable.stringsdict index aa772835..77000804 100644 --- a/damus/ja.lproj/Localizable.stringsdict +++ b/damus/ja.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,13 +13,13 @@ NSStringFormatValueTypeKey d other - %d その他のNote + ... %d その他のNote ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -27,8 +29,6 @@ other フォロワー - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -61,7 +61,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - %@%#@OTHERS@ にリプライ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -69,15 +69,13 @@ NSStringFormatValueTypeKey d other - & %d その他 - zero - + %2$@ & %1$d その他にリプライ replying_to_two_and_others NSStringLocalizedFormatKey - %@, %@%#@OTHERS@ にリプライ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -85,9 +83,7 @@ NSStringFormatValueTypeKey d other - & %d その他 - zero - + %2$@, %3$@ & %1$d その他 にリプライ reposts_count diff --git a/damus/lv-LV.lproj/Localizable.stringsdict b/damus/lv-LV.lproj/Localizable.stringsdict index 3826fe22..62b358ea 100644 --- a/damus/lv-LV.lproj/Localizable.stringsdict +++ b/damus/lv-LV.lproj/Localizable.stringsdict @@ -4,164 +4,164 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - one - %d cita ziņa - other - %d citas ziņas zero - %d other notes + ... %d other notes ... + one + ... %d cita ziņa ... + other + ... %d citas ziņas ... - NSStringLocalizedFormatKey - ··· Ziņas ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + Followers one Sekotājs other Sekotāji - zero - Followers - NSStringLocalizedFormatKey - Sekotāji reactions_count NSStringLocalizedFormatKey - Reakcijas + %#@REACTIONS@ REACTIONS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + Reactions one Reakcija other Reakcijas - zero - Reactions relays_count NSStringLocalizedFormatKey - Releji + %#@RELAYS@ RELAYS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + Relays one Relejs other Releji - zero - Relays replying_to_one_and_others NSStringLocalizedFormatKey - Atbildot %@% #Citam + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - one - & %d cits - other - & %d citiem zero - + Atbildot %2$@ & %1$d others + one + Atbildot %2$@ & %1$d cits + other + Atbildot %2$@ & %1$d citiem replying_to_two_and_others NSStringLocalizedFormatKey - Atbildot %@, %@%#Citiem + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - one - & %d cits - other - & %d citiem zero - + Atbildot %2$@, %3$@ & %1$d others + one + Atbildot %2$@, %3$@ & %1$d cits + other + Atbildot %2$@, %3$@ & %1$d citiem reposts_count NSStringLocalizedFormatKey - Pārpublicējumi + %#@REPOSTS@ REPOSTS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + Reposts one Pārpublicēt other Pārpublicējumi - zero - Reposts sats_count NSStringLocalizedFormatKey - %1$#@Sats@ + %1$#@SATS@ SATS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey @ + zero + %2$@ sats one %2$@ sati other %2$@ sati - zero - %2$@ sats zaps_count NSStringLocalizedFormatKey - Zapi + %#@ZAPS@ ZAPS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + zero + Zaps one Zaps other Zapi - zero - Zaps diff --git a/damus/nl.lproj/InfoPlist.strings b/damus/nl.lproj/InfoPlist.strings index 0c767650..b9448498 100644 Binary files a/damus/nl.lproj/InfoPlist.strings and b/damus/nl.lproj/InfoPlist.strings differ diff --git a/damus/nl.lproj/Localizable.strings b/damus/nl.lproj/Localizable.strings index 62b9a7c6..9f36f305 100644 Binary files a/damus/nl.lproj/Localizable.strings and b/damus/nl.lproj/Localizable.strings differ diff --git a/damus/nl.lproj/Localizable.stringsdict b/damus/nl.lproj/Localizable.stringsdict index 46cc1793..a8a1c3fa 100644 --- a/damus/nl.lproj/Localizable.stringsdict +++ b/damus/nl.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,15 @@ NSStringFormatValueTypeKey d one - %d andere notitie + ... %d andere notitie ... other - %d andere notities + ... %d andere notities ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -31,8 +33,6 @@ other Volgers - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -69,7 +69,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Antwoord aan %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +77,15 @@ NSStringFormatValueTypeKey d one - en %d andere gebruiker + Antwoord aan %2$@ en %1$d andere gebruiker other - en %d andere gebruikers - zero - + Antwoord aan %2$@ en %1$d andere gebruikers replying_to_two_and_others NSStringLocalizedFormatKey - Antwoord aan %@ en %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +93,9 @@ NSStringFormatValueTypeKey d one - en %d andere gebruiker + Antwoord aan %2$@, %3$@ en %1$d andere gebruiker other - en %d andere gebruikers - zero - + Antwoord aan %2$@, %3$@ en %1$d andere gebruikers reposts_count diff --git a/damus/pl-PL.lproj/InfoPlist.strings b/damus/pl-PL.lproj/InfoPlist.strings index 1e32453f..2c29301f 100644 Binary files a/damus/pl-PL.lproj/InfoPlist.strings and b/damus/pl-PL.lproj/InfoPlist.strings differ diff --git a/damus/pl-PL.lproj/Localizable.strings b/damus/pl-PL.lproj/Localizable.strings index 4bb37cd9..0358950a 100644 Binary files a/damus/pl-PL.lproj/Localizable.strings and b/damus/pl-PL.lproj/Localizable.strings differ diff --git a/damus/pl-PL.lproj/Localizable.stringsdict b/damus/pl-PL.lproj/Localizable.stringsdict index 62ffdee0..3993449c 100644 --- a/damus/pl-PL.lproj/Localizable.stringsdict +++ b/damus/pl-PL.lproj/Localizable.stringsdict @@ -4,43 +4,43 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - %d other notes - many - %d other notes one - %d inna notatka + ... %d inna notatka ... + few + ... %d other notes ... + many + ... %d other notes ... other - %d inne notatki + ... %d inne notatki ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Obserwujący few Followers many Followers - one - Obserwujący other Obserwujący - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -52,12 +52,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Reakcja few Reactions many Reactions - one - Reakcja other Reakcje @@ -72,12 +72,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Przekaźnik few Relays many Relays - one - Przekaźnik other Przekaźników @@ -85,45 +85,41 @@ replying_to_one_and_others NSStringLocalizedFormatKey - W odpowiedzi do %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - i %d innej osobie + W odpowiedzi do %2$@ i %1$d innej osobie + few + W odpowiedzi do %2$@ i %1$d others + many + W odpowiedzi do %2$@ i %1$d others other - i %d innym osobom - zero - + W odpowiedzi do %2$@ i %1$d innym osobom replying_to_two_and_others NSStringLocalizedFormatKey - W odpowiedzi do %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - i %d innej osoba + Replying to %2$@, %3$@ i %1$d innej osoba + few + Replying to %2$@, %3$@ i %1$d others + many + Replying to %2$@, %3$@ i %1$d others other - i %d innym osobom - zero - + Replying to %2$@, %3$@ i %1$d innym osobom reposts_count @@ -136,12 +132,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Podany dalej few Reposts many Reposts - one - Podany dalej other Podane dalej @@ -156,12 +152,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey @ + one + %2$@ sat few %2$@ sats many %2$@ sats - one - %2$@ sat other %2$@ satoszy @@ -176,12 +172,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Zap few Zaps many Zaps - one - Zap other Zapów diff --git a/damus/pt-PT.lproj/InfoPlist.strings b/damus/pt-PT.lproj/InfoPlist.strings index 954203d2..488ca234 100644 Binary files a/damus/pt-PT.lproj/InfoPlist.strings and b/damus/pt-PT.lproj/InfoPlist.strings differ diff --git a/damus/pt-PT.lproj/Localizable.strings b/damus/pt-PT.lproj/Localizable.strings index aabd2a1b..cc720db3 100644 Binary files a/damus/pt-PT.lproj/Localizable.strings and b/damus/pt-PT.lproj/Localizable.strings differ diff --git a/damus/pt-PT.lproj/Localizable.stringsdict b/damus/pt-PT.lproj/Localizable.stringsdict index b7d5e0f2..0dbd2f25 100644 --- a/damus/pt-PT.lproj/Localizable.stringsdict +++ b/damus/pt-PT.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,17 @@ NSStringFormatValueTypeKey d one - %d outro note + ... %d outro note ... + many + ... %d outros notes ... other - %d outros notes + ... %d outros notes ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -28,11 +32,11 @@ d one Seguidor + many + Seguidores other Seguidores - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -46,6 +50,8 @@ d one Reação + many + Reações other Reações @@ -62,6 +68,8 @@ d one Relay + many + Relays other Relays @@ -69,7 +77,7 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Respondendo a %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +85,17 @@ NSStringFormatValueTypeKey d one - & %d outros + Respondendo a %2$@ & %1$d outros + many + Respondendo a %2$@ & %1$d outros other - & %d outros - zero - + Respondendo a %2$@ & %1$d outros replying_to_two_and_others NSStringLocalizedFormatKey - Respondendo a %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +103,11 @@ NSStringFormatValueTypeKey d one - & %d outros + Respondendo a %2$@, %3$@ & %1$d outros + many + Respondendo a %2$@, %3$@ & %1$d outros other - & %d outros - zero - + Respondendo a %2$@, %3$@ & %1$d outros reposts_count @@ -114,6 +122,8 @@ d one Repost + many + Reposts other Reposts @@ -130,6 +140,8 @@ @ one %2$@ sat + many + %2$@ sats other %2$@ sats @@ -146,6 +158,8 @@ d one Zap + many + Zaps other Zaps diff --git a/damus/ru.lproj/InfoPlist.strings b/damus/ru.lproj/InfoPlist.strings index 2542bda0..a2a87354 100644 Binary files a/damus/ru.lproj/InfoPlist.strings and b/damus/ru.lproj/InfoPlist.strings differ diff --git a/damus/ru.lproj/Localizable.stringsdict b/damus/ru.lproj/Localizable.stringsdict index 36ada760..f34a9b9c 100644 --- a/damus/ru.lproj/Localizable.stringsdict +++ b/damus/ru.lproj/Localizable.stringsdict @@ -4,43 +4,43 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - %d other notes - many - %d other notes one - %d другая заметка + ... %d другая заметка ... + few + ... %d other notes ... + many + ... %d other notes ... other - %d другие заметки + ... %d другие заметки ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Подписчик few Followers many Followers - one - Подписчик other Подписчики - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -52,12 +52,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Реакция few Reactions many Reactions - one - Реакция other Реакции @@ -72,12 +72,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Релей few Relays many Relays - one - Релей other Релеи @@ -85,45 +85,41 @@ replying_to_one_and_others NSStringLocalizedFormatKey - Replying to %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - и %d другим + В ответ %2$@ и %1$d другим + few + В ответ %2$@ и %1$d others + many + В ответ %2$@ и %1$d others other - и %d другие - zero - + В ответ %2$@ и %1$d другие replying_to_two_and_others NSStringLocalizedFormatKey - В ответ %@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others one - и %d другим + В ответ %2$@, %3$@ и %1$d другим + few + В ответ %2$@, %3$@ и %1$d others + many + В ответ %2$@, %3$@ и %1$d others other - и %d другие - zero - + В ответ %2$@, %3$@ и %1$d другие reposts_count @@ -136,12 +132,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Репост few Reposts many Reposts - one - Репост other Репосты @@ -156,12 +152,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey @ + one + %2$@ сат few %2$@ sats many %2$@ sats - one - %2$@ сат other %2$@ сат @@ -176,12 +172,12 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d + one + Зап few Zaps many Zaps - one - Зап other Запы diff --git a/damus/tr-TR.lproj/InfoPlist.strings b/damus/tr-TR.lproj/InfoPlist.strings index cbd763a5..49847167 100644 Binary files a/damus/tr-TR.lproj/InfoPlist.strings and b/damus/tr-TR.lproj/InfoPlist.strings differ diff --git a/damus/tr-TR.lproj/Localizable.strings b/damus/tr-TR.lproj/Localizable.strings index efe3f4b1..49a39729 100644 Binary files a/damus/tr-TR.lproj/Localizable.strings and b/damus/tr-TR.lproj/Localizable.strings differ diff --git a/damus/tr-TR.lproj/Localizable.stringsdict b/damus/tr-TR.lproj/Localizable.stringsdict index 8a8198bb..8f4ad99a 100644 --- a/damus/tr-TR.lproj/Localizable.stringsdict +++ b/damus/tr-TR.lproj/Localizable.stringsdict @@ -4,6 +4,8 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey @@ -11,15 +13,15 @@ NSStringFormatValueTypeKey d one - %d diğer not + ... %d diğer not ... other - %d diğer notlar + ... %d diğer notlar ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey @@ -29,10 +31,8 @@ one Takipçi other - Takipçiler + Takipçi - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -47,7 +47,7 @@ one Tepki other - Tepkiler + Tepki relays_count @@ -63,13 +63,13 @@ one Röle other - Röleler + Röle replying_to_one_and_others NSStringLocalizedFormatKey - %@%#@OTHERS@'lara yanıt + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -77,17 +77,15 @@ NSStringFormatValueTypeKey d one - & %d diğer + %2$@ & %1$d diğer'lara yanıt other - & %d diğerleri - zero - + %2$@ & %1$d ve diğerlerine yanıt replying_to_two_and_others NSStringLocalizedFormatKey - %@, %@%#@OTHERS@'a yanıt + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey @@ -95,11 +93,9 @@ NSStringFormatValueTypeKey d one - & %d diğer + %2$@, %3$@ & %1$d diğer'a yanıt other - & %d diğerleri - zero - + %2$@, %3$@ & %1$d ve diğerlerine yanıt reposts_count @@ -115,7 +111,7 @@ one Yineleme other - Yinelemeler + Yineleme sats_count @@ -131,7 +127,7 @@ one %2$@ sat other - %2$@ sat + %2$@ sats zaps_count diff --git a/damus/uk.lproj/InfoPlist.strings b/damus/uk.lproj/InfoPlist.strings new file mode 100644 index 00000000..e9a74c71 Binary files /dev/null and b/damus/uk.lproj/InfoPlist.strings differ diff --git a/damus/zh-CN.lproj/InfoPlist.strings b/damus/zh-CN.lproj/InfoPlist.strings index 8ccb59cf..77008a38 100644 Binary files a/damus/zh-CN.lproj/InfoPlist.strings and b/damus/zh-CN.lproj/InfoPlist.strings differ diff --git a/damus/zh-CN.lproj/Localizable.strings b/damus/zh-CN.lproj/Localizable.strings index 6fef0c49..3b11b987 100644 Binary files a/damus/zh-CN.lproj/Localizable.strings and b/damus/zh-CN.lproj/Localizable.strings differ diff --git a/damus/zh-CN.lproj/Localizable.stringsdict b/damus/zh-CN.lproj/Localizable.stringsdict index 776dabcb..455bb872 100644 --- a/damus/zh-CN.lproj/Localizable.stringsdict +++ b/damus/zh-CN.lproj/Localizable.stringsdict @@ -4,51 +4,31 @@ collapsed_event_view_other_notes + NSStringLocalizedFormatKey + %#@NOTES@ NOTES NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - %d other notes - many - %d other notes - one - %d 条更多推文 other - %d 条更多推文 - two - %d other notes - zero - %d other notes + ... %d 条更多推 ... - NSStringLocalizedFormatKey - ··· %#@NOTES@ ··· followers_count + NSStringLocalizedFormatKey + %#@FOLLOWERS@ FOLLOWERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - Followers - many - Followers - one - 粉丝 other 粉丝 - two - Followers - zero - Followers - NSStringLocalizedFormatKey - %#@FOLLOWERS@ reactions_count @@ -60,18 +40,8 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - Reactions - many - Reactions - one - 回应 other 回应 - two - Reactions - zero - Reactions relays_count @@ -84,66 +54,36 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - Relays - many - Relays - one - 中继器 other 中继器 - two - Relays - zero - Relays replying_to_one_and_others NSStringLocalizedFormatKey - 正在回复%@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others - one - & %d 个其他用户 other - & %d 个其他用户 - two - & %d others - zero - + 正在回复%2$@ & %1$d 个其他用户 replying_to_two_and_others NSStringLocalizedFormatKey - 正在回复%@, %@%#@OTHERS@ + %#@OTHERS@ OTHERS NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d - few - & %d others - many - & %d others - one - & %d 个其他用户 other - & %d 个其他用户 - two - & %d others - zero - + 正在回复%2$@, %3$@ & %1$d 个其他用户 reposts_count @@ -156,18 +96,8 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - Reposts - many - Reposts - one - 转发 other 转发 - two - Reposts - zero - Reposts sats_count @@ -180,18 +110,8 @@ NSStringPluralRuleType NSStringFormatValueTypeKey @ - few - %2$@ sats - many - %2$@ sats - one - %2$@ 聪 other %2$@ 聪 - two - %2$@ sats - zero - %2$@ sats zaps_count @@ -204,18 +124,8 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - few - Zaps - many - Zaps - one - 电击 other 电击 - two - Zaps - zero - Zaps diff --git a/damus/zh-HK.lproj/InfoPlist.strings b/damus/zh-HK.lproj/InfoPlist.strings new file mode 100644 index 00000000..79907ec3 Binary files /dev/null and b/damus/zh-HK.lproj/InfoPlist.strings differ diff --git a/damus/zh-HK.lproj/Localizable.strings b/damus/zh-HK.lproj/Localizable.strings new file mode 100644 index 00000000..34a72f67 Binary files /dev/null and b/damus/zh-HK.lproj/Localizable.strings differ diff --git a/damus/zh-HK.lproj/Localizable.stringsdict b/damus/zh-HK.lproj/Localizable.stringsdict new file mode 100644 index 00000000..6339886b --- /dev/null +++ b/damus/zh-HK.lproj/Localizable.stringsdict @@ -0,0 +1,132 @@ + + + + + collapsed_event_view_other_notes + + NSStringLocalizedFormatKey + %#@NOTES@ + NOTES + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + ...還有%d 条便條... + + + followers_count + + NSStringLocalizedFormatKey + %#@FOLLOWERS@ + FOLLOWERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 粉絲 + + + reactions_count + + NSStringLocalizedFormatKey + %#@REACTIONS@ + REACTIONS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 回應 + + + relays_count + + NSStringLocalizedFormatKey + %#@RELAYS@ + RELAYS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 中繼器 + + + replying_to_one_and_others + + NSStringLocalizedFormatKey + %#@OTHERS@ + OTHERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 正在回覆%2$@ & %1$d 個其他用戶 + + + replying_to_two_and_others + + NSStringLocalizedFormatKey + %#@OTHERS@ + OTHERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 正在回覆%2$@, %3$@ & %1$d 個其他用戶 + + + reposts_count + + NSStringLocalizedFormatKey + %#@REPOSTS@ + REPOSTS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 轉發 + + + sats_count + + NSStringLocalizedFormatKey + %1$#@SATS@ + SATS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + @ + other + %2$@ 聰 + + + zaps_count + + NSStringLocalizedFormatKey + %#@ZAPS@ + ZAPS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 電擊 + + + + diff --git a/damus/zh-TW.lproj/InfoPlist.strings b/damus/zh-TW.lproj/InfoPlist.strings new file mode 100644 index 00000000..79907ec3 Binary files /dev/null and b/damus/zh-TW.lproj/InfoPlist.strings differ diff --git a/damus/zh-TW.lproj/Localizable.strings b/damus/zh-TW.lproj/Localizable.strings new file mode 100644 index 00000000..8e304b45 Binary files /dev/null and b/damus/zh-TW.lproj/Localizable.strings differ diff --git a/damus/zh-TW.lproj/Localizable.stringsdict b/damus/zh-TW.lproj/Localizable.stringsdict new file mode 100644 index 00000000..6339886b --- /dev/null +++ b/damus/zh-TW.lproj/Localizable.stringsdict @@ -0,0 +1,132 @@ + + + + + collapsed_event_view_other_notes + + NSStringLocalizedFormatKey + %#@NOTES@ + NOTES + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + ...還有%d 条便條... + + + followers_count + + NSStringLocalizedFormatKey + %#@FOLLOWERS@ + FOLLOWERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 粉絲 + + + reactions_count + + NSStringLocalizedFormatKey + %#@REACTIONS@ + REACTIONS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 回應 + + + relays_count + + NSStringLocalizedFormatKey + %#@RELAYS@ + RELAYS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 中繼器 + + + replying_to_one_and_others + + NSStringLocalizedFormatKey + %#@OTHERS@ + OTHERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 正在回覆%2$@ & %1$d 個其他用戶 + + + replying_to_two_and_others + + NSStringLocalizedFormatKey + %#@OTHERS@ + OTHERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 正在回覆%2$@, %3$@ & %1$d 個其他用戶 + + + reposts_count + + NSStringLocalizedFormatKey + %#@REPOSTS@ + REPOSTS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 轉發 + + + sats_count + + NSStringLocalizedFormatKey + %1$#@SATS@ + SATS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + @ + other + %2$@ 聰 + + + zaps_count + + NSStringLocalizedFormatKey + %#@ZAPS@ + ZAPS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + 電擊 + + + + diff --git a/damusTests/EventDetailBarTests.swift b/damusTests/EventDetailBarTests.swift new file mode 100644 index 00000000..0b9a08a0 --- /dev/null +++ b/damusTests/EventDetailBarTests.swift @@ -0,0 +1,56 @@ +// +// EventDetailBarTests.swift +// damusTests +// +// Created by Terry Yiu on 2/24/23. +// + +import XCTest +@testable import damus + +final class EventDetailBarTests: XCTestCase { + + let enUsLocale = Locale(identifier: "en-US") + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testRepostsCountString() throws { + XCTAssertEqual(repostsCountString(0, locale: enUsLocale), "Reposts") + XCTAssertEqual(repostsCountString(1, locale: enUsLocale), "Repost") + XCTAssertEqual(repostsCountString(2, locale: enUsLocale), "Reposts") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(repostsCountString(count, locale: $0)) + } + } + } + + func testReactionsCountString() throws { + XCTAssertEqual(reactionsCountString(0, locale: enUsLocale), "Reactions") + XCTAssertEqual(reactionsCountString(1, locale: enUsLocale), "Reaction") + XCTAssertEqual(reactionsCountString(2, locale: enUsLocale), "Reactions") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(reactionsCountString(count, locale: $0)) + } + } + } + + func testZapssCountString() throws { + XCTAssertEqual(zapsCountString(0, locale: enUsLocale), "Zaps") + XCTAssertEqual(zapsCountString(1, locale: enUsLocale), "Zap") + XCTAssertEqual(zapsCountString(2, locale: enUsLocale), "Zaps") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(zapsCountString(count, locale: $0)) + } + } + } + +} diff --git a/damusTests/EventGroupViewTests.swift b/damusTests/EventGroupViewTests.swift new file mode 100644 index 00000000..6ccdcf6c --- /dev/null +++ b/damusTests/EventGroupViewTests.swift @@ -0,0 +1,42 @@ +// +// EventGroupViewTests.swift +// damusTests +// +// Created by Terry Yiu on 2/26/23. +// + +import XCTest +@testable import damus + +final class EventGroupViewTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testReactingToText() throws { + let enUsLocale = Locale(identifier: "en-US") + let damusState = test_damus_state() + + let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" + let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, locale: enUsLocale), "??") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, locale: enUsLocale), "pk1:pk1 reposted a post you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a post you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, locale: enUsLocale), "pk1:pk1 and 2 others reposted a post you were tagged in") + + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, locale: $0), "??") + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, locale: $0)) + } + } + +} diff --git a/damusTests/FormatTests.swift b/damusTests/FormatTests.swift index 148878f6..35fa79e6 100644 --- a/damusTests/FormatTests.swift +++ b/damusTests/FormatTests.swift @@ -34,4 +34,23 @@ final class FormatTests: XCTestCase { XCTAssertEqual(format_msats_abbrev(1000), "1") } + func testFormatMsats() throws { + let enUsLocale = Locale(identifier: "en-US") + XCTAssertEqual(format_msats(0, locale: enUsLocale), "0 sats") + XCTAssertEqual(format_msats(1, locale: enUsLocale), "0.001 sats") + XCTAssertEqual(format_msats(1000, locale: enUsLocale), "1 sat") + XCTAssertEqual(format_msats(1001, locale: enUsLocale), "1.001 sats") + XCTAssertEqual(format_msats(2000, locale: enUsLocale), "2 sats") + XCTAssertEqual(format_msats(123456789, locale: enUsLocale), "123,456.789 sats") + // Sanity check that function call does not throw in any supported locale as the string format accepts arguments, and a mismatched format in any one of the locales could break the app. + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(format_msats(0, locale: $0)) + XCTAssertNoThrow(format_msats(1, locale: $0)) + XCTAssertNoThrow(format_msats(1000, locale: $0)) + XCTAssertNoThrow(format_msats(1001, locale: $0)) + XCTAssertNoThrow(format_msats(2000, locale: $0)) + XCTAssertNoThrow(format_msats(123456789, locale: $0)) + } + } + } diff --git a/damusTests/ProfileViewTests.swift b/damusTests/ProfileViewTests.swift new file mode 100644 index 00000000..637d8041 --- /dev/null +++ b/damusTests/ProfileViewTests.swift @@ -0,0 +1,45 @@ +// +// ProfileViewTests.swift +// damusTests +// +// Created by Terry Yiu on 2/24/23. +// + +import XCTest +@testable import damus + +final class ProfileViewTests: XCTestCase { + + let enUsLocale = Locale(identifier: "en-US") + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testFollowersCountString() throws { + XCTAssertEqual(followersCountString(0, locale: enUsLocale), "Followers") + XCTAssertEqual(followersCountString(1, locale: enUsLocale), "Follower") + XCTAssertEqual(followersCountString(2, locale: enUsLocale), "Followers") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(followersCountString(count, locale: $0)) + } + } + } + + func testRelaysCountString() throws { + XCTAssertEqual(relaysCountString(0, locale: enUsLocale), "Relays") + XCTAssertEqual(relaysCountString(1, locale: enUsLocale), "Relay") + XCTAssertEqual(relaysCountString(2, locale: enUsLocale), "Relays") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(relaysCountString(count, locale: $0)) + } + } + } + +} diff --git a/damusTests/ReplyDescriptionTests.swift b/damusTests/ReplyDescriptionTests.swift new file mode 100644 index 00000000..4dbf226d --- /dev/null +++ b/damusTests/ReplyDescriptionTests.swift @@ -0,0 +1,87 @@ +// +// ReplyDescriptionTests.swift +// damusTests +// +// Created by Terry Yiu on 2/21/23. +// + +import XCTest +@testable import damus + +final class ReplyDescriptionTests: XCTestCase { + + let enUsLocale = Locale(identifier: "en-US") + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + // Test that English strings work properly with argument substitution and pluralization, and that other locales don't crash. + func testReplyDesc() throws { + let profiles = test_damus_state().profiles + + let replyingToSelfEvent = test_event + XCTAssertEqual(reply_desc(profiles: profiles, event: replyingToSelfEvent, locale: enUsLocale), "Replying to self") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reply_desc(profiles: profiles, event: replyingToSelfEvent, locale: $0)) + } + + let replyingToOne = NostrEvent( + content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool", + pubkey: "pk", + tags: [["e", "123"], ["p", "123"]], + createdAt: Int64(Date().timeIntervalSince1970 - 100) + ) + XCTAssertEqual(reply_desc(profiles: profiles, event: replyingToOne, locale: enUsLocale), "Replying to \(Profile.displayName(profile: nil, pubkey: "123"))") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reply_desc(profiles: profiles, event: replyingToOne, locale: $0)) + } + + let replyingToTwo = NostrEvent( + content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool", + pubkey: "pk", + tags: [["e", "123"], ["p", "123"], ["p", "456"]], + createdAt: Int64(Date().timeIntervalSince1970 - 100) + ) + XCTAssertEqual(reply_desc(profiles: profiles, event: replyingToTwo, locale: enUsLocale), "Replying to \(Profile.displayName(profile: nil, pubkey: "456")) & \(Profile.displayName(profile: nil, pubkey: "123"))") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reply_desc(profiles: profiles, event: replyingToTwo, locale: $0)) + } + + let replyingToTwoAndOneOther = NostrEvent( + content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool", + pubkey: "pk", + tags: [["e", "123"], ["p", "123"], ["p", "456"], ["p", "789"]], + createdAt: Int64(Date().timeIntervalSince1970 - 100) + ) + XCTAssertEqual(reply_desc(profiles: profiles, event: replyingToTwoAndOneOther, locale: enUsLocale), "Replying to \(Profile.displayName(profile: nil, pubkey: "789")), \(Profile.displayName(profile: nil, pubkey: "456")) & 1 other") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reply_desc(profiles: profiles, event: replyingToTwoAndOneOther, locale: $0)) + } + + for othersCount in 2...10 { + var tags: [[String]] = [["e", "123"]] + for i in 1...othersCount { + tags.append(["p", "\(i)"]) + } + tags.append(["p", "456"]) + tags.append(["p", "789"]) + + let replyingToTwoAndMultipleOthers = NostrEvent( + content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool", + pubkey: "pk", + tags: tags, + createdAt: Int64(Date().timeIntervalSince1970 - 100) + ) + XCTAssertEqual(reply_desc(profiles: profiles, event: replyingToTwoAndMultipleOthers, locale: enUsLocale), "Replying to \(Profile.displayName(profile: nil, pubkey: "789")), \(Profile.displayName(profile: nil, pubkey: "456")) & \(othersCount) others") + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reply_desc(profiles: profiles, event: replyingToTwoAndMultipleOthers, locale: $0)) + } + } + } + +} diff --git a/damusTests/TimeAgoTests.swift b/damusTests/TimeAgoTests.swift index 0b1af3a3..f11c92a8 100644 --- a/damusTests/TimeAgoTests.swift +++ b/damusTests/TimeAgoTests.swift @@ -11,7 +11,7 @@ import XCTest final class TimeAgoTests: XCTestCase { func testTimeAgoSince() { - let locale = Locale(identifier: "en_US") + let locale = Locale(identifier: "en-US") let calendar = locale.calendar XCTAssertEqual(time_ago_since(Date.now, calendar), "now") diff --git a/devtools/export-source-translation.sh b/devtools/export-source-translation.sh index b1a9b49f..e443317d 100755 --- a/devtools/export-source-translation.sh +++ b/devtools/export-source-translation.sh @@ -4,4 +4,4 @@ xcodebuild -exportLocalizations -project damus.xcodeproj -localizationPath "damus" -exportLanguage en-US # Generates all SwiftUI Text() wrapped localized strings. -genstrings -o "damus/en-US.xcloc/Source Contents/damus/en-US.lproj/" -SwiftUI **/*.swift +genstrings -a -d -o "damus/en-US.xcloc/Source Contents/damus/en-US.lproj/" -SwiftUI **/*.swift diff --git a/transifex.yml b/transifex.yml index ab7c9b4c..f109a5ca 100644 --- a/transifex.yml +++ b/transifex.yml @@ -4,25 +4,27 @@ git: file_format: STRINGSDICT source_file_extension: stringsdict source_language: en_US - source_file_dir: damus/en-US.xcloc/Source Contents/damus/en-US.lproj/ - translation_files_expression: damus/.lproj/ + source_file_dir: 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/' + translation_files_expression: 'damus/.lproj/' - filter_type: dir file_format: STRINGS source_file_extension: strings source_language: en_US - source_file_dir: damus/en-US.xcloc/Source Contents/damus/en-US.lproj/ - translation_files_expression: damus/.lproj/ + source_file_dir: 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/' + translation_files_expression: 'damus/.lproj/' -settings: - language_mapping: - de_AT: de-AT - el_GR: el-GR - es_419: es-419 - fr_FR: fr-FR - it_IT: it-IT - lv_LV: lv-LV - pl_PL: pl-PL - pt_PT: pt-PT - tr_TR: tr-TR - zh_CN: zh-CN + settings: + language_mapping: + en_US: en-US + el_GR: el-GR + es_419: es-419 + fr_FR: fr-FR + it_IT: it-IT + lv_LV: lv-LV + pl_PL: pl-PL + pt_PT: pt-PT + tr_TR: tr-TR + zh_CN: zh-CN + zh_HK: zh-HK + zh_TW: zh-TW \ No newline at end of file