Merge remote-tracking branch 'upstream/master' into change-emoji-component
This commit is contained in:
@@ -34,6 +34,9 @@
|
|||||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||||
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
|
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
|
||||||
|
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
|
||||||
|
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */; };
|
||||||
|
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */; };
|
||||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||||
@@ -494,6 +497,9 @@
|
|||||||
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
||||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = D789D11F2AFEFBF20083A7AB /* secp256k1 */; };
|
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = D789D11F2AFEFBF20083A7AB /* secp256k1 */; };
|
||||||
D78CD5982B8990300014D539 /* DamusAppNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */; };
|
D78CD5982B8990300014D539 /* DamusAppNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */; };
|
||||||
|
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
||||||
|
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
||||||
|
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
||||||
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||||
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
|
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
|
||||||
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||||
@@ -822,6 +828,9 @@
|
|||||||
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = "<group>"; };
|
3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = "<group>"; };
|
||||||
|
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEventView.swift; sourceTree = "<group>"; };
|
||||||
|
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomThreadView.swift; sourceTree = "<group>"; };
|
||||||
|
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
||||||
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
||||||
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
|
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
|
||||||
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
|
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
@@ -1410,6 +1419,8 @@
|
|||||||
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
|
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
|
||||||
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
|
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
|
||||||
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
||||||
|
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
||||||
|
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
||||||
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
||||||
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
||||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
||||||
@@ -1475,6 +1486,7 @@
|
|||||||
files = (
|
files = (
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||||
|
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */,
|
||||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||||
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
|
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -1996,6 +2008,7 @@
|
|||||||
4C75EFA227FA576C0006080F /* Views */ = {
|
4C75EFA227FA576C0006080F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D78DB85D2C20FE9E00F0AB12 /* Chat */,
|
||||||
D71AC4CA2BA8E3320076268E /* Extensions */,
|
D71AC4CA2BA8E3320076268E /* Extensions */,
|
||||||
BA3759952ABCCF360018D73B /* Camera */,
|
BA3759952ABCCF360018D73B /* Camera */,
|
||||||
F71694E82A66221E001F4053 /* Onboarding */,
|
F71694E82A66221E001F4053 /* Onboarding */,
|
||||||
@@ -2692,6 +2705,7 @@
|
|||||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||||
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
|
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
|
||||||
D72E12772BEED22400F4F781 /* Array.swift */,
|
D72E12772BEED22400F4F781 /* Array.swift */,
|
||||||
|
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2760,6 +2774,17 @@
|
|||||||
path = Purple;
|
path = Purple;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D78DB85D2C20FE9E00F0AB12 /* Chat */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */,
|
||||||
|
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */,
|
||||||
|
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */,
|
||||||
|
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */,
|
||||||
|
);
|
||||||
|
path = Chat;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2842,6 +2867,7 @@
|
|||||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||||
4C27C9312A64766F007DBC75 /* MarkdownUI */,
|
4C27C9312A64766F007DBC75 /* MarkdownUI */,
|
||||||
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
|
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
|
||||||
|
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
|
||||||
);
|
);
|
||||||
productName = damus;
|
productName = damus;
|
||||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||||
@@ -2982,6 +3008,7 @@
|
|||||||
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
|
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
|
||||||
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
|
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
|
||||||
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
|
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
|
||||||
|
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -3235,6 +3262,7 @@
|
|||||||
E02429952B7E97740088B16C /* CameraController.swift in Sources */,
|
E02429952B7E97740088B16C /* CameraController.swift in Sources */,
|
||||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||||
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */,
|
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */,
|
||||||
|
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
|
||||||
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
||||||
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */,
|
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */,
|
||||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||||
@@ -3243,6 +3271,7 @@
|
|||||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
|
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
|
||||||
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
|
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
|
||||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||||
|
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */,
|
||||||
D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */,
|
D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */,
|
||||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||||
@@ -3331,6 +3360,7 @@
|
|||||||
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
||||||
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
||||||
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
||||||
|
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */,
|
||||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||||
@@ -3390,6 +3420,7 @@
|
|||||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||||
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
|
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
|
||||||
|
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
|
||||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||||
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
||||||
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
||||||
@@ -3499,6 +3530,7 @@
|
|||||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
||||||
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */,
|
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */,
|
||||||
|
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */,
|
||||||
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
||||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
||||||
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
||||||
@@ -4324,6 +4356,14 @@
|
|||||||
minimumVersion = 0.2.26;
|
minimumVersion = 0.2.26;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/aheze/SwipeActions";
|
||||||
|
requirement = {
|
||||||
|
kind = exactVersion;
|
||||||
|
version = 1.1.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
|
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing";
|
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing";
|
||||||
@@ -4360,6 +4400,11 @@
|
|||||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||||
productName = secp256k1;
|
productName = secp256k1;
|
||||||
};
|
};
|
||||||
|
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */;
|
||||||
|
productName = SwipeActions;
|
||||||
|
};
|
||||||
D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = {
|
D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "8c4ef1768f3ccbc97566169b88ce1c3a482207b6d3e394f2584f8749d5314dc6",
|
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "emojikit",
|
"identity" : "emojikit",
|
||||||
@@ -88,6 +88,15 @@
|
|||||||
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
|
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
|
||||||
"version" : "0.1.2"
|
"version" : "0.1.2"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swipeactions",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/aheze/SwipeActions",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
|
||||||
|
"version" : "1.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xD7",
|
||||||
|
"green" : "0xD1",
|
||||||
|
"red" : "0xD1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x13",
|
||||||
|
"green" : "0x11",
|
||||||
|
"red" : "0x11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF9",
|
||||||
|
"green" : "0xF3",
|
||||||
|
"red" : "0xF3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x25",
|
||||||
|
"green" : "0x22",
|
||||||
|
"red" : "0x22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "244",
|
||||||
|
"green" : "218",
|
||||||
|
"red" : "244"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "92",
|
||||||
|
"green" : "45",
|
||||||
|
"red" : "93"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "236",
|
||||||
|
"green" : "194",
|
||||||
|
"red" : "238"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "109",
|
||||||
|
"green" : "49",
|
||||||
|
"red" : "111"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "197",
|
||||||
|
"green" : "67",
|
||||||
|
"red" : "204"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "255",
|
||||||
|
"green" : "194",
|
||||||
|
"red" : "255"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,11 @@ import SwiftUI
|
|||||||
|
|
||||||
class DamusColors {
|
class DamusColors {
|
||||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||||
|
static let adaptableGrey2 = Color("DamusAdaptableGrey 2")
|
||||||
|
static let adaptableLighterGrey = Color("DamusAdaptableLighterGrey")
|
||||||
|
static let adaptablePurpleBackground = Color("DamusAdaptablePurpleBackground 1")
|
||||||
|
static let adaptablePurpleBackground2 = Color("DamusAdaptablePurpleBackground 2")
|
||||||
|
static let adaptablePurpleForeground = Color("DamusAdaptablePurpleForeground")
|
||||||
static let adaptableBlack = Color("DamusAdaptableBlack")
|
static let adaptableBlack = Color("DamusAdaptableBlack")
|
||||||
static let adaptableWhite = Color("DamusAdaptableWhite")
|
static let adaptableWhite = Color("DamusAdaptableWhite")
|
||||||
static let white = Color("DamusWhite")
|
static let white = Color("DamusWhite")
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import SwiftUI
|
|||||||
struct TruncatedText: View {
|
struct TruncatedText: View {
|
||||||
let text: CompatibleText
|
let text: CompatibleText
|
||||||
let maxChars: Int
|
let maxChars: Int
|
||||||
|
let show_show_more_button: Bool
|
||||||
|
|
||||||
init(text: CompatibleText, maxChars: Int = 280) {
|
init(text: CompatibleText, maxChars: Int = 280, show_show_more_button: Bool) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.maxChars = maxChars
|
self.maxChars = maxChars
|
||||||
|
self.show_show_more_button = show_show_more_button
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -29,8 +31,10 @@ struct TruncatedText: View {
|
|||||||
|
|
||||||
if truncatedAttributedString != nil {
|
if truncatedAttributedString != nil {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
if self.show_show_more_button {
|
||||||
.allowsHitTesting(false)
|
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,10 +42,10 @@ struct TruncatedText: View {
|
|||||||
struct TruncatedText_Previews: PreviewProvider {
|
struct TruncatedText_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 100) {
|
VStack(spacing: 100) {
|
||||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
|
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"), show_show_more_button: true)
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
|
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"), show_show_more_button: true)
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ class ThreadModel: ObservableObject {
|
|||||||
self.original_event = event
|
self.original_event = event
|
||||||
add_event(event, keypair: damus_state.keypair)
|
add_event(event, keypair: damus_state.keypair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func events() -> [NostrEvent] {
|
||||||
|
return Array(event_map).sorted(by: { a, b in
|
||||||
|
return a.created_at < b.created_at
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var is_original: Bool {
|
var is_original: Bool {
|
||||||
return original_event.id == event.id
|
return original_event.id == event.id
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -97,13 +97,13 @@ class EventCache {
|
|||||||
// TODO: remove me and change code to use ndb directly
|
// TODO: remove me and change code to use ndb directly
|
||||||
private let ndb: Ndb
|
private let ndb: Ndb
|
||||||
private var events: [NoteId: NostrEvent] = [:]
|
private var events: [NoteId: NostrEvent] = [:]
|
||||||
private var replies = ReplyMap()
|
|
||||||
private var cancellable: AnyCancellable?
|
private var cancellable: AnyCancellable?
|
||||||
private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key
|
private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key
|
||||||
private var event_data: [NoteId: EventData] = [:]
|
private var event_data: [NoteId: EventData] = [:]
|
||||||
|
var replies = ReplyMap()
|
||||||
|
|
||||||
//private var thread_latest: [String: Int64]
|
//private var thread_latest: [String: Int64]
|
||||||
|
|
||||||
init(ndb: Ndb) {
|
init(ndb: Ndb) {
|
||||||
self.ndb = ndb
|
self.ndb = ndb
|
||||||
cancellable = NotificationCenter.default.publisher(
|
cancellable = NotificationCenter.default.publisher(
|
||||||
@@ -187,7 +187,7 @@ class EventCache {
|
|||||||
replies.add(id: reply, reply_id: ev.id)
|
replies.add(id: reply, reply_id: ev.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func child_events(event: NostrEvent) -> [NostrEvent] {
|
func child_events(event: NostrEvent) -> [NostrEvent] {
|
||||||
guard let xs = replies.lookup(event.id) else {
|
guard let xs = replies.lookup(event.id) else {
|
||||||
return []
|
return []
|
||||||
|
|||||||
27
damus/Util/Extensions/VectorMath.swift
Normal file
27
damus/Util/Extensions/VectorMath.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// VectorMath.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-06-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGPoint {
|
||||||
|
/// Summing a vector to a point
|
||||||
|
static func +(lhs: CGPoint, rhs: CGVector) -> CGPoint {
|
||||||
|
return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracting a vector from a point
|
||||||
|
static func -(lhs: CGPoint, rhs: CGVector) -> CGPoint {
|
||||||
|
return CGPoint(x: lhs.x - rhs.dx, y: lhs.y - rhs.dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGVector {
|
||||||
|
/// Multiplying a vector by a scalar
|
||||||
|
static func *(lhs: CGVector, rhs: CGFloat) -> CGVector {
|
||||||
|
return CGVector(dx: lhs.dx * rhs, dy: lhs.dy * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,8 @@ enum Route: Hashable {
|
|||||||
case .FirstAidSettings(settings: let settings):
|
case .FirstAidSettings(settings: let settings):
|
||||||
FirstAidSettingsView(damus_state: damusState, settings: settings)
|
FirstAidSettingsView(damus_state: damusState, settings: settings)
|
||||||
case .Thread(let thread):
|
case .Thread(let thread):
|
||||||
ThreadView(state: damusState, thread: thread)
|
ChatroomThreadView(damus: damusState, thread: thread)
|
||||||
|
//ThreadView(state: damusState, thread: thread)
|
||||||
case .Reposts(let reposts):
|
case .Reposts(let reposts):
|
||||||
RepostsView(damus_state: damusState, model: reposts)
|
RepostsView(damus_state: damusState, model: reposts)
|
||||||
case .QuoteReposts(let quote_reposts):
|
case .QuoteReposts(let quote_reposts):
|
||||||
|
|||||||
@@ -8,12 +8,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import EmojiPicker
|
import EmojiPicker
|
||||||
import EmojiKit
|
import EmojiKit
|
||||||
|
import SwipeActions
|
||||||
|
|
||||||
struct EventActionBar: View {
|
struct EventActionBar: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
let userProfile : ProfileModel
|
let userProfile : ProfileModel
|
||||||
|
let swipe_context: SwipeContext?
|
||||||
|
let options: Options
|
||||||
|
|
||||||
// just used for previews
|
// just used for previews
|
||||||
@State var show_share_sheet: Bool = false
|
@State var show_share_sheet: Bool = false
|
||||||
@@ -23,12 +26,14 @@ struct EventActionBar: View {
|
|||||||
@State private var selectedEmoji: Emoji? = nil
|
@State private var selectedEmoji: Emoji? = nil
|
||||||
|
|
||||||
@ObservedObject var bar: ActionBarModel
|
@ObservedObject var bar: ActionBarModel
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
|
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, options: Options = [], swipe_context: SwipeContext? = nil) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.event = event
|
self.event = event
|
||||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||||
self.userProfile = ProfileModel(pubkey: event.pubkey, damus: damus_state)
|
self.userProfile = ProfileModel(pubkey: event.pubkey, damus: damus_state)
|
||||||
|
self.options = options
|
||||||
|
self.swipe_context = swipe_context
|
||||||
}
|
}
|
||||||
|
|
||||||
var lnurl: String? {
|
var lnurl: String? {
|
||||||
@@ -45,60 +50,176 @@ struct EventActionBar: View {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var space_if_spread: AnyView {
|
||||||
HStack {
|
if options.contains(.no_spread) {
|
||||||
if damus_state.keypair.privkey != nil {
|
return AnyView(EmptyView())
|
||||||
HStack(spacing: 4) {
|
|
||||||
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
|
|
||||||
notify(.compose(.replying_to(event)))
|
|
||||||
}
|
|
||||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
|
||||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
|
||||||
.font(.footnote.weight(.medium))
|
|
||||||
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
|
|
||||||
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
|
|
||||||
self.show_repost_action = true
|
|
||||||
}
|
|
||||||
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
|
|
||||||
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
|
||||||
.font(.footnote.weight(.medium))
|
|
||||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if show_like {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in
|
|
||||||
if bar.liked {
|
|
||||||
//notify(.delete, bar.our_like)
|
|
||||||
} else {
|
|
||||||
send_like(emoji: emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
|
||||||
.font(.footnote.weight(.medium))
|
|
||||||
.nip05_colorized(gradient: bar.liked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let lnurl = self.lnurl {
|
|
||||||
Spacer()
|
|
||||||
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
EventActionButton(img: "upload", col: Color.gray) {
|
|
||||||
show_share_action = true
|
|
||||||
}
|
|
||||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return AnyView(Spacer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Swipe action menu buttons
|
||||||
|
|
||||||
|
var reply_swipe_button: some View {
|
||||||
|
SwipeAction(systemImage: "arrowshape.turn.up.left.fill", backgroundColor: DamusColors.adaptableGrey) {
|
||||||
|
notify(.compose(.replying_to(event)))
|
||||||
|
self.swipe_context?.state.wrappedValue = .closed
|
||||||
|
}
|
||||||
|
.allowSwipeToTrigger()
|
||||||
|
.swipeButtonStyle()
|
||||||
|
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var repost_swipe_button: some View {
|
||||||
|
SwipeAction(image: "repost", backgroundColor: DamusColors.adaptableGrey) {
|
||||||
|
self.show_repost_action = true
|
||||||
|
self.swipe_context?.state.wrappedValue = .closed
|
||||||
|
}
|
||||||
|
.swipeButtonStyle()
|
||||||
|
.accessibilityLabel(NSLocalizedString("Repost or quote this note", comment: "Accessibility label for repost/quote button"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var like_swipe_button: some View {
|
||||||
|
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
|
||||||
|
send_like(emoji: damus_state.settings.default_emoji_reaction)
|
||||||
|
self.swipe_context?.state.wrappedValue = .closed
|
||||||
|
}
|
||||||
|
.swipeButtonStyle()
|
||||||
|
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var share_swipe_button: some View {
|
||||||
|
SwipeAction(image: "upload", backgroundColor: DamusColors.adaptableGrey) {
|
||||||
|
show_share_action = true
|
||||||
|
self.swipe_context?.state.wrappedValue = .closed
|
||||||
|
}
|
||||||
|
.swipeButtonStyle()
|
||||||
|
.accessibilityLabel(NSLocalizedString("Share externally", comment: "Accessibility label for external share button"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Bar buttons
|
||||||
|
|
||||||
|
var reply_button: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||||
|
notify(.compose(.replying_to(event)))
|
||||||
|
}
|
||||||
|
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||||
|
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||||
|
.font(.footnote.weight(.medium))
|
||||||
|
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var repost_button: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
|
||||||
|
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
|
||||||
|
self.show_repost_action = true
|
||||||
|
}
|
||||||
|
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
|
||||||
|
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||||
|
.font(.footnote.weight(.medium))
|
||||||
|
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var like_button: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in
|
||||||
|
if bar.liked {
|
||||||
|
//notify(.delete, bar.our_like)
|
||||||
|
} else {
|
||||||
|
send_like(emoji: emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||||
|
.font(.footnote.weight(.medium))
|
||||||
|
.nip05_colorized(gradient: bar.liked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var share_button: some View {
|
||||||
|
EventActionButton(img: "upload", col: Color.gray) {
|
||||||
|
show_share_action = true
|
||||||
|
}
|
||||||
|
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Main views
|
||||||
|
|
||||||
|
var swipe_action_menu_content: some View {
|
||||||
|
Group {
|
||||||
|
self.reply_swipe_button
|
||||||
|
self.repost_swipe_button
|
||||||
|
if show_like {
|
||||||
|
self.like_swipe_button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var swipe_action_menu_reverse_content: some View {
|
||||||
|
Group {
|
||||||
|
if show_like {
|
||||||
|
self.like_swipe_button
|
||||||
|
}
|
||||||
|
self.repost_swipe_button
|
||||||
|
self.reply_swipe_button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var action_bar_content: some View {
|
||||||
|
let hide_items_without_activity = options.contains(.hide_items_without_activity)
|
||||||
|
let should_hide_chat_bubble = hide_items_without_activity && bar.replies == 0
|
||||||
|
let should_hide_repost = hide_items_without_activity && bar.boosts == 0
|
||||||
|
let should_hide_reactions = hide_items_without_activity && bar.likes == 0
|
||||||
|
let zap_model = self.damus_state.events.get_cache_data(self.event.id).zaps_model
|
||||||
|
let should_hide_zap = hide_items_without_activity && zap_model.zap_total > 0
|
||||||
|
let should_hide_share_button = hide_items_without_activity
|
||||||
|
|
||||||
|
return HStack(spacing: options.contains(.no_spread) ? 10 : 0) {
|
||||||
|
if damus_state.keypair.privkey != nil && !should_hide_chat_bubble {
|
||||||
|
self.reply_button
|
||||||
|
}
|
||||||
|
|
||||||
|
if !should_hide_repost {
|
||||||
|
self.space_if_spread
|
||||||
|
self.repost_button
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_like && !should_hide_reactions {
|
||||||
|
self.space_if_spread
|
||||||
|
self.like_button
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lnurl = self.lnurl, !should_hide_zap {
|
||||||
|
self.space_if_spread
|
||||||
|
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: zap_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !should_hide_share_button {
|
||||||
|
self.space_if_spread
|
||||||
|
self.share_button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some View {
|
||||||
|
if options.contains(.swipe_action_menu) {
|
||||||
|
AnyView(self.swipe_action_menu_content)
|
||||||
|
}
|
||||||
|
else if options.contains(.swipe_action_menu_reverse) {
|
||||||
|
AnyView(self.swipe_action_menu_reverse_content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AnyView(self.action_bar_content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
self.content
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.bar.update(damus: damus_state, evid: self.event.id)
|
self.bar.update(damus: damus_state, evid: self.event.id)
|
||||||
}
|
}
|
||||||
@@ -151,6 +272,17 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
damus_state.postbox.send(like_ev)
|
damus_state.postbox.send(like_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Helper structures
|
||||||
|
|
||||||
|
struct Options: OptionSet {
|
||||||
|
let rawValue: UInt32
|
||||||
|
|
||||||
|
static let no_spread = Options(rawValue: 1 << 0)
|
||||||
|
static let hide_items_without_activity = Options(rawValue: 1 << 1)
|
||||||
|
static let swipe_action_menu = Options(rawValue: 1 << 2)
|
||||||
|
static let swipe_action_menu_reverse = Options(rawValue: 1 << 3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -178,7 +310,6 @@ struct LikeButton: View {
|
|||||||
|
|
||||||
@State private var isReactionsVisible = false
|
@State private var isReactionsVisible = false
|
||||||
|
|
||||||
// @State private var selectedEmoji: String = ""
|
|
||||||
@State private var selectedEmoji: Emoji?
|
@State private var selectedEmoji: Emoji?
|
||||||
|
|
||||||
// Following four are Shaka animation properties
|
// Following four are Shaka animation properties
|
||||||
@@ -287,7 +418,6 @@ struct LikeButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct EventActionBar_Previews: PreviewProvider {
|
struct EventActionBar_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let ds = test_damus_state
|
let ds = test_damus_state
|
||||||
@@ -312,7 +442,44 @@ struct EventActionBar_Previews: PreviewProvider {
|
|||||||
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
|
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
|
||||||
|
|
||||||
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
|
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
|
||||||
|
|
||||||
|
EventActionBar(damus_state: ds, event: ev, bar: bar, options: [.no_spread])
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Helpers
|
||||||
|
|
||||||
|
fileprivate struct SwipeButtonStyle: ViewModifier {
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.frame(width: 50, height: 50)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.overlay(Circle().stroke(Color.damusAdaptableGrey2, lineWidth: 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension View {
|
||||||
|
func swipeButtonStyle() -> some View {
|
||||||
|
modifier(SwipeButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Needed extensions for SwipeAction
|
||||||
|
|
||||||
|
public extension SwipeAction where Label == Image, Background == Color {
|
||||||
|
init(
|
||||||
|
image: String,
|
||||||
|
backgroundColor: Color = Color.primary.opacity(0.1),
|
||||||
|
highlightOpacity: Double = 0.5,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.init(action: action) { highlight in
|
||||||
|
Image(image)
|
||||||
|
} background: { highlight in
|
||||||
|
backgroundColor
|
||||||
|
.opacity(highlight ? highlightOpacity : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
184
damus/Views/Chat/ChatBubbleView.swift
Normal file
184
damus/Views/Chat/ChatBubbleView.swift
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
//
|
||||||
|
// ChatBubbleView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-06-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Use this view to display content inside of a custom-designed chat bubble shape.
|
||||||
|
struct ChatBubble<T: View, U: ShapeStyle, V: View>: View {
|
||||||
|
/// The direction at which the chat bubble tip will be pointing towards
|
||||||
|
let direction: Direction
|
||||||
|
let stroke_content: U
|
||||||
|
let stroke_style: StrokeStyle
|
||||||
|
let background_style: V
|
||||||
|
@ViewBuilder let content: T
|
||||||
|
|
||||||
|
// Constants, which are loosely tied to `OFFSET_X` and `OFFSET_Y`
|
||||||
|
let OFFSET_X_PADDING: CGFloat = 6
|
||||||
|
let OFFSET_Y_BOTTOM_PADDING: CGFloat = 3
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
self.content
|
||||||
|
.padding(direction == .left ? .leading : .trailing, OFFSET_X_PADDING)
|
||||||
|
.padding(.bottom, OFFSET_Y_BOTTOM_PADDING)
|
||||||
|
.background(self.background_style)
|
||||||
|
.clipShape(
|
||||||
|
BubbleShape(direction: self.direction)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
BubbleShape(direction: self.direction)
|
||||||
|
.stroke(self.stroke_content, style: self.stroke_style)
|
||||||
|
)
|
||||||
|
.padding(direction == .left ? .leading : .trailing, -OFFSET_X_PADDING)
|
||||||
|
.padding(.bottom, -OFFSET_Y_BOTTOM_PADDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
case right
|
||||||
|
case left
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BubbleShape: Shape {
|
||||||
|
/// The direction at which the chat bubble tip will be pointing towards
|
||||||
|
let direction: Direction
|
||||||
|
|
||||||
|
// MARK: Constant parameters that defines the shape and look of the chat bubbles
|
||||||
|
|
||||||
|
/// The corner radius of the round edges
|
||||||
|
let CORNER_RADIUS: CGFloat = 10
|
||||||
|
/// The height of the chat bubble tip detail
|
||||||
|
let DETAIL_HEIGHT: CGFloat = 10
|
||||||
|
/// The horizontal distance between the chat bubble tip and the vertical edge of the bubble
|
||||||
|
let OFFSET_X: CGFloat = 7
|
||||||
|
/// The vertical distance between the chat bubble tip and the bottom edge of the bubble
|
||||||
|
let OFFSET_Y: CGFloat = 5
|
||||||
|
/// Value between 0 and 1 that determines curvature of the upper chat bubble curve detail
|
||||||
|
let DETAIL_CURVE_FACTOR: CGFloat = 0.75
|
||||||
|
/// Value between 0 and 1 that determines curvature of the lower chat bubble curve detail
|
||||||
|
let LOWER_DETAIL_CURVE_FACTOR: CGFloat = 0.4
|
||||||
|
/// The horizontal distance between the chat bubble tip and the point at which the lower chat bubble curve detail attaches to the bottom of the chat bubble
|
||||||
|
let LOWER_DETAIL_ATTACHMENT_OFFSET_X: CGFloat = 20
|
||||||
|
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
return self.direction == .left ? self.draw_left_bubble(in: rect) : self.draw_right_bubble(in: rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw_left_bubble(in rect: CGRect) -> Path {
|
||||||
|
return Path { p in
|
||||||
|
// Start at the top left, just below the end of the corner radius
|
||||||
|
let start = CGPoint(x: OFFSET_X, y: CORNER_RADIUS)
|
||||||
|
// Left edge
|
||||||
|
p.move(to: start)
|
||||||
|
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
|
||||||
|
// Draw the chat bubble tip
|
||||||
|
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
|
||||||
|
let tip_of_bubble = CGPoint(x: 0, y: rect.height)
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: tip_of_bubble,
|
||||||
|
control: CGPoint(x: 0, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
|
||||||
|
)
|
||||||
|
let lower_detail_attachment = CGPoint(x: LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
|
||||||
|
p.addCurve(
|
||||||
|
to: lower_detail_attachment,
|
||||||
|
control1: tip_of_bubble + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
|
||||||
|
control2: lower_detail_attachment - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
|
||||||
|
)
|
||||||
|
// Draw the bottom edge
|
||||||
|
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS, y: rect.height - OFFSET_Y))
|
||||||
|
// Draw the bottom right round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: CGPoint(x: rect.width, y: rect.height - OFFSET_Y - CORNER_RADIUS),
|
||||||
|
control: CGPoint(x: rect.width, y: rect.height - OFFSET_Y)
|
||||||
|
)
|
||||||
|
// Draw right edge
|
||||||
|
p.addLine(to: CGPoint(x: rect.width, y: CORNER_RADIUS))
|
||||||
|
// Draw top right round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: CGPoint(x: rect.width - CORNER_RADIUS, y: 0),
|
||||||
|
control: CGPoint(x: rect.width, y: 0)
|
||||||
|
)
|
||||||
|
// Draw top edge
|
||||||
|
p.addLine(to: CGPoint(x: CORNER_RADIUS + OFFSET_X, y: 0))
|
||||||
|
// Draw top left round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: start,
|
||||||
|
control: CGPoint(x: OFFSET_X, y: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw_right_bubble(in rect: CGRect) -> Path {
|
||||||
|
return Path { p in
|
||||||
|
// Start at the top right, just below the end of the corner radius
|
||||||
|
let right_edge = rect.width - OFFSET_X
|
||||||
|
let start = CGPoint(x: right_edge, y: CORNER_RADIUS)
|
||||||
|
p.move(to: start)
|
||||||
|
// Right edge
|
||||||
|
p.addLine(to: CGPoint(x: right_edge, y: rect.height - DETAIL_HEIGHT))
|
||||||
|
// Draw the chat bubble tip
|
||||||
|
let tip_of_bubble = CGPoint(x: rect.width, y: rect.height)
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: tip_of_bubble,
|
||||||
|
control: CGPoint(x: rect.width, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: -OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
|
||||||
|
)
|
||||||
|
let lower_detail_attachment = CGPoint(x: rect.width - LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
|
||||||
|
p.addCurve(
|
||||||
|
to: lower_detail_attachment,
|
||||||
|
control1: tip_of_bubble - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
|
||||||
|
control2: lower_detail_attachment + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
|
||||||
|
)
|
||||||
|
// Draw the bottom edge
|
||||||
|
p.addLine(to: CGPoint(x: CORNER_RADIUS, y: rect.height - OFFSET_Y))
|
||||||
|
// Draw the bottom left round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: CGPoint(x: 0, y: rect.height - OFFSET_Y - CORNER_RADIUS),
|
||||||
|
control: CGPoint(x: 0, y: rect.height - OFFSET_Y)
|
||||||
|
)
|
||||||
|
// Draw left edge
|
||||||
|
p.addLine(to: CGPoint(x: 0, y: CORNER_RADIUS))
|
||||||
|
// Draw top right round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: CGPoint(x: CORNER_RADIUS, y: 0),
|
||||||
|
control: CGPoint(x: 0, y: 0)
|
||||||
|
)
|
||||||
|
// Draw top edge
|
||||||
|
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS - OFFSET_X, y: 0))
|
||||||
|
// Draw top left round corner
|
||||||
|
p.addQuadCurve(
|
||||||
|
to: start,
|
||||||
|
control: CGPoint(x: rect.width - OFFSET_X, y: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack {
|
||||||
|
ChatBubble(
|
||||||
|
direction: .left,
|
||||||
|
stroke_content: Color.accentColor.opacity(0),
|
||||||
|
stroke_style: .init(lineWidth: 4),
|
||||||
|
background_style: Color.accentColor
|
||||||
|
) {
|
||||||
|
Text("Hello there")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
ChatBubble(
|
||||||
|
direction: .right,
|
||||||
|
stroke_content: Color.accentColor.opacity(0),
|
||||||
|
stroke_style: .init(lineWidth: 4),
|
||||||
|
background_style: Color.accentColor
|
||||||
|
) {
|
||||||
|
Text("Hello there")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
324
damus/Views/Chat/ChatEventView.swift
Normal file
324
damus/Views/Chat/ChatEventView.swift
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
//
|
||||||
|
// ChatView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-04-19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import EmojiKit
|
||||||
|
import EmojiPicker
|
||||||
|
import SwipeActions
|
||||||
|
|
||||||
|
fileprivate let CORNER_RADIUS: CGFloat = 10
|
||||||
|
|
||||||
|
struct ChatEventView: View {
|
||||||
|
// MARK: Parameters
|
||||||
|
let event: NostrEvent
|
||||||
|
let selected_event: NostrEvent
|
||||||
|
let prev_ev: NostrEvent?
|
||||||
|
let next_ev: NostrEvent?
|
||||||
|
let damus_state: DamusState
|
||||||
|
var thread: ThreadModel
|
||||||
|
let scroll_to_event: ((_ id: NoteId) -> Void)?
|
||||||
|
let focus_event: (() -> Void)?
|
||||||
|
let highlight_bubble: Bool
|
||||||
|
|
||||||
|
// MARK: long-press reaction control objects
|
||||||
|
/// Whether the user is actively pressing the view
|
||||||
|
@State var is_pressing = false
|
||||||
|
/// The dispatched work item scheduled by a timer to bounce the event bubble and show the emoji selector
|
||||||
|
@State var long_press_bounce_work_item: DispatchWorkItem?
|
||||||
|
@State var popover_state: PopoverState = .closed {
|
||||||
|
didSet {
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: popover_state == .open_emoji_selector ? .heavy : .light)
|
||||||
|
generator.impactOccurred()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@State var selected_emoji: Emoji?
|
||||||
|
|
||||||
|
@State private var isOnTopHalfOfScreen: Bool = false
|
||||||
|
@ObservedObject var bar: ActionBarModel
|
||||||
|
|
||||||
|
enum PopoverState: String {
|
||||||
|
case closed
|
||||||
|
case open_emoji_selector
|
||||||
|
}
|
||||||
|
|
||||||
|
var just_started: Bool {
|
||||||
|
return prev_ev == nil || prev_ev!.pubkey != event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func next_replies_to_this() -> Bool {
|
||||||
|
guard let next = next_ev else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return damus_state.events.replies.lookup(next.id) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_reply_to_prev(ref_id: NoteId) -> Bool {
|
||||||
|
guard let prev = prev_ev else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let rep = damus_state.events.replies.lookup(event.id) {
|
||||||
|
return rep.contains(prev.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var disable_animation: Bool {
|
||||||
|
self.damus_state.settings.disable_animation
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply_quote_options: EventViewOptions {
|
||||||
|
return [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate, .no_media]
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile_picture_view: some View {
|
||||||
|
VStack {
|
||||||
|
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||||
|
.onTapGesture {
|
||||||
|
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
var by_other_user: Bool {
|
||||||
|
return event.pubkey != damus_state.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_ours: Bool { return !by_other_user }
|
||||||
|
|
||||||
|
var event_bubble: some View {
|
||||||
|
ChatBubble(
|
||||||
|
direction: is_ours ? .right : .left,
|
||||||
|
stroke_content: Color.accentColor.opacity(highlight_bubble ? 1 : 0),
|
||||||
|
stroke_style: .init(lineWidth: 4),
|
||||||
|
background_style: by_other_user ? DamusColors.adaptableGrey : DamusColors.adaptablePurpleBackground
|
||||||
|
) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
if by_other_user {
|
||||||
|
HStack {
|
||||||
|
ProfileName(pubkey: event.pubkey, damus: damus_state)
|
||||||
|
.onTapGesture {
|
||||||
|
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||||
|
}
|
||||||
|
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let replying_to = event.direct_replies(),
|
||||||
|
replying_to != selected_event.id {
|
||||||
|
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: replying_to, state: damus_state, thread: thread, options: reply_quote_options)
|
||||||
|
.background(is_ours ? DamusColors.adaptablePurpleBackground2 : DamusColors.adaptableGrey2)
|
||||||
|
.foregroundColor(is_ours ? Color.damusAdaptablePurpleForeground : Color.damusAdaptableBlack)
|
||||||
|
.cornerRadius(5)
|
||||||
|
.onTapGesture {
|
||||||
|
self.scroll_to_event?(replying_to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let blur_images = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||||
|
NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: [])
|
||||||
|
.padding(2)
|
||||||
|
}
|
||||||
|
.frame(minWidth: 150, alignment: is_ours ? .trailing : .leading)
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
.tint(is_ours ? Color.white : Color.accentColor)
|
||||||
|
.overlay(
|
||||||
|
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
self.action_bar
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if popover_state == .closed {
|
||||||
|
focus_event?()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
popover_state = .closed
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
generator.impactOccurred()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var event_bubble_with_long_press_interaction: some View {
|
||||||
|
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
|
||||||
|
self.event_bubble
|
||||||
|
.sheet(isPresented: Binding(get: { popover_state == .open_emoji_selector }, set: { new_state in
|
||||||
|
withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
|
||||||
|
popover_state = new_state == true ? .open_emoji_selector : .closed
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
NavigationView {
|
||||||
|
EmojiPickerView(selectedEmoji: $selected_emoji, emojiProvider: damus_state.emoji_provider)
|
||||||
|
}.presentationDetents([.medium, .large])
|
||||||
|
}
|
||||||
|
.onChange(of: selected_emoji) { newSelectedEmoji in
|
||||||
|
if let newSelectedEmoji {
|
||||||
|
send_like(emoji: newSelectedEmoji.value)
|
||||||
|
popover_state = .closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scaleEffect(self.popover_state == .open_emoji_selector ? 1.08 : is_pressing ? 1.02 : 1)
|
||||||
|
.shadow(color: (is_pressing || self.popover_state == .open_emoji_selector) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state == .open_emoji_selector) ? 8 : 0, y: (is_pressing || self.popover_state == .open_emoji_selector) ? 15 : 0)
|
||||||
|
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: {
|
||||||
|
long_press_bounce_work_item?.cancel()
|
||||||
|
}, onPressingChanged: { is_pressing in
|
||||||
|
withAnimation(is_pressing ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
|
||||||
|
self.is_pressing = is_pressing
|
||||||
|
if popover_state != .closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.is_pressing {
|
||||||
|
let item = DispatchWorkItem {
|
||||||
|
// Ensure the action is performed only if the condition is still valid
|
||||||
|
if self.is_pressing {
|
||||||
|
withAnimation(.bouncy(duration: 0.2, extraBounce: 0.35)) {
|
||||||
|
popover_state = .open_emoji_selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long_press_bounce_work_item = item
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.background(
|
||||||
|
GeometryReader { geometry in
|
||||||
|
EmptyView()
|
||||||
|
.onAppear {
|
||||||
|
let eventActionBarY = geometry.frame(in: .global).midY
|
||||||
|
let screenMidY = UIScreen.main.bounds.midY
|
||||||
|
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
|
||||||
|
}
|
||||||
|
.onChange(of: geometry.frame(in: .global).midY) { newY in
|
||||||
|
let screenMidY = UIScreen.main.bounds.midY
|
||||||
|
self.isOnTopHalfOfScreen = newY > screenMidY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func send_like(emoji: String) {
|
||||||
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
|
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bar.our_like = like_ev
|
||||||
|
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
generator.impactOccurred()
|
||||||
|
|
||||||
|
damus_state.postbox.send(like_ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
var action_bar: some View {
|
||||||
|
return Group {
|
||||||
|
if !bar.is_empty {
|
||||||
|
HStack {
|
||||||
|
if by_other_user {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
EventActionBar(damus_state: damus_state, event: event, bar: bar, options: [.no_spread, .hide_items_without_activity])
|
||||||
|
.padding(10)
|
||||||
|
.background(DamusColors.adaptableLighterGrey)
|
||||||
|
.disabled(true)
|
||||||
|
.cornerRadius(100)
|
||||||
|
.overlay(RoundedRectangle(cornerSize: CGSize(width: 100, height: 100)).stroke(DamusColors.adaptableWhite, lineWidth: 1))
|
||||||
|
.shadow(color: Color.black.opacity(0.05),radius: 3, y: 3)
|
||||||
|
.scaleEffect(0.7, anchor: is_ours ? .leading : .trailing)
|
||||||
|
if !by_other_user {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, -20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var event_bubble_with_long_press_and_swipe_interactions: some View {
|
||||||
|
Group {
|
||||||
|
SwipeView {
|
||||||
|
self.event_bubble_with_long_press_interaction
|
||||||
|
} leadingActions: { context in
|
||||||
|
EventActionBar(
|
||||||
|
damus_state: damus_state,
|
||||||
|
event: event,
|
||||||
|
bar: bar,
|
||||||
|
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
|
||||||
|
swipe_context: context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.swipeSpacing(-20)
|
||||||
|
.swipeActionsStyle(.mask)
|
||||||
|
.swipeMinimumDistance(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some View {
|
||||||
|
return VStack {
|
||||||
|
HStack(alignment: .bottom, spacing: 4) {
|
||||||
|
if by_other_user {
|
||||||
|
self.profile_picture_view
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.event_bubble_with_long_press_and_swipe_interactions
|
||||||
|
|
||||||
|
if !by_other_user {
|
||||||
|
self.profile_picture_view
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.id(event.id)
|
||||||
|
.padding([.bottom], bar.is_empty ? 6 : 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if [.boost, .zap, .longform].contains(where: { event.known_kind == $0 }) {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
self.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var toggle_thread_view: Notification.Name {
|
||||||
|
return Notification.Name("convert_to_thread")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||||
|
return ChatEventView(event: test_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||||
|
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||||
|
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true, bar: bar)
|
||||||
|
}
|
||||||
195
damus/Views/Chat/ChatroomThreadView.swift
Normal file
195
damus/Views/Chat/ChatroomThreadView.swift
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
//
|
||||||
|
// ChatroomView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-04-19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwipeActions
|
||||||
|
|
||||||
|
struct ChatroomThreadView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var once: Bool = false
|
||||||
|
let damus: DamusState
|
||||||
|
@ObservedObject var thread: ThreadModel
|
||||||
|
@State var selected_note_id: NoteId? = nil
|
||||||
|
@State var user_just_posted_flag: Bool = false
|
||||||
|
@Namespace private var animation
|
||||||
|
|
||||||
|
@State var parent_events: [NostrEvent] = []
|
||||||
|
@State var sorted_child_events: [NostrEvent] = []
|
||||||
|
|
||||||
|
func compute_events(selected_event: NostrEvent? = nil) {
|
||||||
|
let selected_event = selected_event ?? thread.event
|
||||||
|
self.parent_events = damus.events.parent_events(event: selected_event, keypair: damus.keypair)
|
||||||
|
let all_recursive_child_events = self.recursive_child_events(event: selected_event)
|
||||||
|
self.sorted_child_events = all_recursive_child_events.filter({
|
||||||
|
should_show_event(event: $0, damus_state: damus) // Hide muted events from chatroom conversation
|
||||||
|
}).sorted(by: { a, b in
|
||||||
|
return a.created_at < b.created_at
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursive_child_events(event: NdbNote) -> [NdbNote] {
|
||||||
|
let immediate_children = damus.events.child_events(event: event)
|
||||||
|
var indirect_children: [NdbNote] = []
|
||||||
|
for immediate_child in immediate_children {
|
||||||
|
indirect_children.append(contentsOf: self.recursive_child_events(event: immediate_child))
|
||||||
|
}
|
||||||
|
return immediate_children + indirect_children
|
||||||
|
}
|
||||||
|
|
||||||
|
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
|
||||||
|
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: .top)
|
||||||
|
selected_note_id = note_id
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
|
||||||
|
withAnimation {
|
||||||
|
selected_note_id = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_active_event(scroller: ScrollViewProxy, ev: NdbNote) {
|
||||||
|
withAnimation {
|
||||||
|
self.compute_events(selected_event: ev)
|
||||||
|
thread.set_active_event(ev, keypair: self.damus.keypair)
|
||||||
|
self.go_to_event(scroller: scroller, note_id: ev.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollViewReader { scroller in
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 8) {
|
||||||
|
// MARK: - Parents events view
|
||||||
|
ForEach(parent_events, id: \.id) { parent_event in
|
||||||
|
EventMutingContainerView(damus_state: damus, event: parent_event) {
|
||||||
|
EventView(damus: damus, event: parent_event)
|
||||||
|
.matchedGeometryEffect(id: parent_event.id.hex(), in: animation, anchor: .center)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.onTapGesture {
|
||||||
|
self.set_active_event(scroller: scroller, ev: parent_event)
|
||||||
|
}
|
||||||
|
.id(parent_event.id)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.padding(.top, 4)
|
||||||
|
.padding(.leading, 25 * 2)
|
||||||
|
|
||||||
|
}.background(GeometryReader { geometry in
|
||||||
|
// get the height and width of the EventView view
|
||||||
|
let eventHeight = geometry.frame(in: .global).height
|
||||||
|
// let eventWidth = geometry.frame(in: .global).width
|
||||||
|
|
||||||
|
// vertical gray line in the background
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.25))
|
||||||
|
.frame(width: 2, height: eventHeight)
|
||||||
|
.offset(x: 40, y: 40)
|
||||||
|
})
|
||||||
|
|
||||||
|
// MARK: - Actual event view
|
||||||
|
EventMutingContainerView(
|
||||||
|
damus_state: damus,
|
||||||
|
event: self.thread.event,
|
||||||
|
muteBox: { event_shown, muted_reason in
|
||||||
|
AnyView(
|
||||||
|
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||||
|
.padding(5)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
SelectedEventView(damus: damus, event: self.thread.event, size: .selected)
|
||||||
|
.matchedGeometryEffect(id: self.thread.event.id.hex(), in: animation, anchor: .center)
|
||||||
|
}
|
||||||
|
.id(self.thread.event.id)
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Children view
|
||||||
|
let events = sorted_child_events
|
||||||
|
let count = events.count
|
||||||
|
SwipeViewGroup {
|
||||||
|
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
|
||||||
|
ChatEventView(event: events[ind],
|
||||||
|
selected_event: self.thread.event,
|
||||||
|
prev_ev: ind > 0 ? events[ind-1] : nil,
|
||||||
|
next_ev: ind == count-1 ? nil : events[ind+1],
|
||||||
|
damus_state: damus,
|
||||||
|
thread: thread,
|
||||||
|
scroll_to_event: { note_id in
|
||||||
|
self.go_to_event(scroller: scroller, note_id: note_id)
|
||||||
|
},
|
||||||
|
focus_event: {
|
||||||
|
self.set_active_event(scroller: scroller, ev: ev)
|
||||||
|
},
|
||||||
|
highlight_bubble: selected_note_id == ev.id,
|
||||||
|
bar: make_actionbar_model(ev: ev.id, damus: damus)
|
||||||
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.id(ev.id)
|
||||||
|
.matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
EndBlock()
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.post), perform: { notify in
|
||||||
|
switch notify {
|
||||||
|
case .post(_):
|
||||||
|
user_just_posted_flag = true
|
||||||
|
case .cancel:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onReceive(thread.objectWillChange) {
|
||||||
|
self.compute_events()
|
||||||
|
if let last_event = thread.events().last, last_event.pubkey == damus.pubkey, user_just_posted_flag {
|
||||||
|
self.go_to_event(scroller: scroller, note_id: last_event.id)
|
||||||
|
user_just_posted_flag = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear() {
|
||||||
|
thread.subscribe()
|
||||||
|
self.compute_events()
|
||||||
|
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
|
||||||
|
}
|
||||||
|
.onDisappear() {
|
||||||
|
thread.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle_thread_view() {
|
||||||
|
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct ChatroomView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
ChatroomThreadView(damus: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state))
|
||||||
|
.previewDisplayName("Test note")
|
||||||
|
|
||||||
|
let test_thread = ThreadModel(event: test_thread_note_1, damus_state: test_damus_state)
|
||||||
|
ChatroomThreadView(damus: test_damus_state, thread: test_thread)
|
||||||
|
.onAppear {
|
||||||
|
test_thread.add_event(test_thread_note_2, keypair: test_keypair)
|
||||||
|
test_thread.add_event(test_thread_note_3, keypair: test_keypair)
|
||||||
|
test_thread.add_event(test_thread_note_4, keypair: test_keypair)
|
||||||
|
test_thread.add_event(test_thread_note_5, keypair: test_keypair)
|
||||||
|
test_thread.add_event(test_thread_note_6, keypair: test_keypair)
|
||||||
|
test_thread.add_event(test_thread_note_7, keypair: test_keypair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||||
|
scroll_to_event(scroller: proxy, id: thread.event.id, delay: 0.1, animate: false)
|
||||||
|
}
|
||||||
70
damus/Views/Chat/ReplyQuoteView.swift
Normal file
70
damus/Views/Chat/ReplyQuoteView.swift
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// ReplyQuoteView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-04-19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReplyQuoteView: View {
|
||||||
|
let keypair: Keypair
|
||||||
|
let quoter: NostrEvent
|
||||||
|
let event_id: NoteId
|
||||||
|
let state: DamusState
|
||||||
|
@ObservedObject var thread: ThreadModel
|
||||||
|
let options: EventViewOptions
|
||||||
|
|
||||||
|
func content(event: NdbNote) -> some View {
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
if should_show_event(event: event, damus_state: state) {
|
||||||
|
ProfilePicView(pubkey: event.pubkey, size: 14, highlight: .reply, profiles: state.profiles, disable_animation: false)
|
||||||
|
let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey)
|
||||||
|
NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .small, options: options)
|
||||||
|
.font(.callout)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.bottom, -7)
|
||||||
|
.padding(.top, -5)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.frame(height: 20)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text("Note you've muted", comment: "Label indicating note has been muted")
|
||||||
|
.italic()
|
||||||
|
.font(.caption)
|
||||||
|
.opacity(0.5)
|
||||||
|
.padding(.bottom, -7)
|
||||||
|
.padding(.top, -5)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.frame(height: 20)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(5)
|
||||||
|
.padding(.leading, 5+3)
|
||||||
|
Rectangle()
|
||||||
|
.foregroundStyle(.accent)
|
||||||
|
.frame(width: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let event = state.events.lookup(event_id) {
|
||||||
|
self.content(event: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReplyQuoteView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let s = test_damus_state
|
||||||
|
let quoter = test_note
|
||||||
|
ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: test_note.id, state: s, thread: ThreadModel(event: quoter, damus_state: s), options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,13 +51,13 @@ struct LongformPreviewBody: View {
|
|||||||
func truncatedText(content: CompatibleText) -> some View {
|
func truncatedText(content: CompatibleText) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if truncate_very_short {
|
if truncate_very_short {
|
||||||
TruncatedText(text: content, maxChars: 140)
|
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
|
||||||
.font(header ? .body : .caption)
|
.font(header ? .body : .caption)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
}
|
}
|
||||||
else if truncate {
|
else if truncate {
|
||||||
TruncatedText(text: content)
|
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
|
||||||
.font(header ? .body : .caption)
|
.font(header ? .body : .caption)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ struct EventViewOptions: OptionSet {
|
|||||||
static let no_mentions = EventViewOptions(rawValue: 1 << 9)
|
static let no_mentions = EventViewOptions(rawValue: 1 << 9)
|
||||||
static let no_media = EventViewOptions(rawValue: 1 << 10)
|
static let no_media = EventViewOptions(rawValue: 1 << 10)
|
||||||
static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11)
|
static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11)
|
||||||
|
static let no_previews = EventViewOptions(rawValue: 1 << 12)
|
||||||
|
static let no_show_more = EventViewOptions(rawValue: 1 << 13)
|
||||||
|
|
||||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||||
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short]
|
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short, .no_previews]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TextEvent: View {
|
struct TextEvent: View {
|
||||||
|
|||||||
@@ -78,11 +78,11 @@ struct NoteContentView: View {
|
|||||||
func truncatedText(content: CompatibleText) -> some View {
|
func truncatedText(content: CompatibleText) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if truncate_very_short {
|
if truncate_very_short {
|
||||||
TruncatedText(text: content, maxChars: 140)
|
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
|
||||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
||||||
}
|
}
|
||||||
else if truncate {
|
else if truncate {
|
||||||
TruncatedText(text: content)
|
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
|
||||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
||||||
} else {
|
} else {
|
||||||
content.text
|
content.text
|
||||||
@@ -185,18 +185,22 @@ struct NoteContentView: View {
|
|||||||
invoicesView(invoices: artifacts.invoices)
|
invoicesView(invoices: artifacts.invoices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if damus_state.settings.media_previews {
|
if damus_state.settings.media_previews, has_previews {
|
||||||
if with_padding {
|
if with_padding {
|
||||||
previewView(links: artifacts.links).padding(.horizontal)
|
previewView(links: artifacts.links).padding(.horizontal)
|
||||||
} else {
|
} else {
|
||||||
previewView(links: artifacts.links)
|
previewView(links: artifacts.links)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var has_previews: Bool {
|
||||||
|
!options.contains(.no_previews)
|
||||||
|
}
|
||||||
|
|
||||||
func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View {
|
func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
load_media = true
|
load_media = true
|
||||||
@@ -397,6 +401,14 @@ struct NoteContentView_Previews: PreviewProvider {
|
|||||||
.border(Color.red)
|
.border(Color.red)
|
||||||
}
|
}
|
||||||
.previewDisplayName("Long-form note")
|
.previewDisplayName("Long-form note")
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .small, options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more])
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Small single-line note")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user