Follow Packs

This PR adds and enables follow packs in the universe view.

Closes: #3012

Changelog-Added: Added follow list kind 39089
Changelog-Added: Added follow pack preview
Changelog-Added: Added follow pack timeline to Universe View
Changelog-Removed: Removed hashtags in Universe View

Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
ericholguin
2025-05-13 20:15:05 -06:00
committed by Daniel D’Aquino
parent f436291209
commit 414c67a919
18 changed files with 774 additions and 22 deletions

View File

@@ -418,11 +418,26 @@
5C0567592C8FBDE30073F23A /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; 5C0567592C8FBDE30073F23A /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; }; 5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; };
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C09FD132DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C09FD142DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; }; 5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; };
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; }; 5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; }; 5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */; }; 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */; };
5C4FA7EC2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7ED2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7EE2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7FB2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FC2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FD2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FF2DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8002DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8012DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8032DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */; };
5C4FA8042DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */; };
5C4FA8052DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; }; 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
@@ -2437,11 +2452,16 @@
5C0567542C8B60C20073F23A /* OffsetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetExtension.swift; sourceTree = "<group>"; }; 5C0567542C8B60C20073F23A /* OffsetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetExtension.swift; sourceTree = "<group>"; };
5C0567572C8FBC560073F23A /* NDBSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NDBSearchView.swift; sourceTree = "<group>"; }; 5C0567572C8FBC560073F23A /* NDBSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NDBSearchView.swift; sourceTree = "<group>"; };
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; };
5C09FD112DF283D200823661 /* FollowPackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackModel.swift; sourceTree = "<group>"; };
5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = "<group>"; }; 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = "<group>"; };
5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; }; 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; }; 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDraftContentView.swift; sourceTree = "<group>"; }; 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDraftContentView.swift; sourceTree = "<group>"; };
5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackEvent.swift; sourceTree = "<group>"; };
5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackView.swift; sourceTree = "<group>"; };
5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackPreview.swift; sourceTree = "<group>"; };
5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackTimeline.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; }; 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
@@ -2808,6 +2828,8 @@
4C0A3F8D280F63FF000448DE /* Models */ = { 4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C09FD112DF283D200823661 /* FollowPackModel.swift */,
5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */,
D73BDB122D71212600D69970 /* NostrNetworkManager */, D73BDB122D71212600D69970 /* NostrNetworkManager */,
D74F43082B23F09300425B75 /* Purple */, D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */, BA3759882ABCCDE30018D73B /* Camera */,
@@ -3614,6 +3636,7 @@
4CC7AAEE297F11B300430951 /* Events */ = { 4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C4FA7FA2DC29C3800CE658C /* FollowPack */,
5CC852A02BDED9970039FFC5 /* Highlight */, 5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */, 4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */, 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
@@ -3929,6 +3952,16 @@
path = Images; path = Images;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5C4FA7FA2DC29C3800CE658C /* FollowPack */ = {
isa = PBXGroup;
children = (
5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */,
5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */,
5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */,
);
path = FollowPack;
sourceTree = "<group>";
};
5CC852A02BDED9970039FFC5 /* Highlight */ = { 5CC852A02BDED9970039FFC5 /* Highlight */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -4655,6 +4688,7 @@
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */, BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */, 4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */,
5C4FA8032DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */, D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
@@ -4799,6 +4833,7 @@
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */, 4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */, D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
5C4FA7ED2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */, 3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */, D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
@@ -4879,6 +4914,7 @@
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */, D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */, 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */,
5C4FA7FF2DC5119300CE658C /* FollowPackPreview.swift in Sources */,
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 */,
@@ -4909,6 +4945,7 @@
3165648B295B70D500C64604 /* LinkView.swift in Sources */, 3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */, 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */, D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */,
5C4FA7FD2DC29C3800CE658C /* FollowPackView.swift in Sources */,
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */, 4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
B533694E2B66D791008A805E /* MutelistManager.swift in Sources */, B533694E2B66D791008A805E /* MutelistManager.swift in Sources */,
@@ -4948,6 +4985,7 @@
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */, 4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */, 5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
5C09FD132DF283D700823661 /* FollowPackModel.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
@@ -5109,6 +5147,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C4FA7FB2DC29C3800CE658C /* FollowPackView.swift in Sources */,
D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */, D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */,
82D6FA9A2CD9820500C925F4 /* ShareViewController.swift in Sources */, 82D6FA9A2CD9820500C925F4 /* ShareViewController.swift in Sources */,
82D6FAA92CD99F7900C925F4 /* FbConstants.swift in Sources */, 82D6FAA92CD99F7900C925F4 /* FbConstants.swift in Sources */,
@@ -5175,6 +5214,7 @@
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */, 82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */, 82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */, 82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
5C4FA8012DC5119300CE658C /* FollowPackPreview.swift in Sources */,
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */, 82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */, 82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */, D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
@@ -5227,6 +5267,7 @@
82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */, 82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */,
82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */, 82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */,
82D6FB112CD99F7900C925F4 /* DamusLightGradient.swift in Sources */, 82D6FB112CD99F7900C925F4 /* DamusLightGradient.swift in Sources */,
5C4FA8042DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
82D6FB132CD99F7900C925F4 /* Shimmer.swift in Sources */, 82D6FB132CD99F7900C925F4 /* Shimmer.swift in Sources */,
82D6FB142CD99F7900C925F4 /* EndBlock.swift in Sources */, 82D6FB142CD99F7900C925F4 /* EndBlock.swift in Sources */,
82D6FB152CD99F7900C925F4 /* ImageCarousel.swift in Sources */, 82D6FB152CD99F7900C925F4 /* ImageCarousel.swift in Sources */,
@@ -5373,6 +5414,7 @@
82D6FB9B2CD99F7900C925F4 /* MutedThreadsManager.swift in Sources */, 82D6FB9B2CD99F7900C925F4 /* MutedThreadsManager.swift in Sources */,
82D6FB9C2CD99F7900C925F4 /* WalletModel.swift in Sources */, 82D6FB9C2CD99F7900C925F4 /* WalletModel.swift in Sources */,
82D6FB9D2CD99F7900C925F4 /* ZapButtonModel.swift in Sources */, 82D6FB9D2CD99F7900C925F4 /* ZapButtonModel.swift in Sources */,
5C09FD142DF283D700823661 /* FollowPackModel.swift in Sources */,
82D6FB9E2CD99F7900C925F4 /* ContentFilters.swift in Sources */, 82D6FB9E2CD99F7900C925F4 /* ContentFilters.swift in Sources */,
82D6FB9F2CD99F7900C925F4 /* DamusCacheManager.swift in Sources */, 82D6FB9F2CD99F7900C925F4 /* DamusCacheManager.swift in Sources */,
82D6FBA02CD99F7900C925F4 /* NotificationsManager.swift in Sources */, 82D6FBA02CD99F7900C925F4 /* NotificationsManager.swift in Sources */,
@@ -5510,6 +5552,7 @@
82D6FC202CD99F7900C925F4 /* RelayType.swift in Sources */, 82D6FC202CD99F7900C925F4 /* RelayType.swift in Sources */,
82D6FC212CD99F7900C925F4 /* SignalView.swift in Sources */, 82D6FC212CD99F7900C925F4 /* SignalView.swift in Sources */,
82D6FC222CD99F7900C925F4 /* RelayPicView.swift in Sources */, 82D6FC222CD99F7900C925F4 /* RelayPicView.swift in Sources */,
5C4FA7EC2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
82D6FC232CD99F7900C925F4 /* UserSearch.swift in Sources */, 82D6FC232CD99F7900C925F4 /* UserSearch.swift in Sources */,
82D6FC242CD99F7900C925F4 /* AddMuteItemView.swift in Sources */, 82D6FC242CD99F7900C925F4 /* AddMuteItemView.swift in Sources */,
82D6FC252CD99F7900C925F4 /* MuteDurationMenu.swift in Sources */, 82D6FC252CD99F7900C925F4 /* MuteDurationMenu.swift in Sources */,
@@ -5680,6 +5723,7 @@
D73E5E602C6A97F4007EB227 /* ImageMetadata.swift in Sources */, D73E5E602C6A97F4007EB227 /* ImageMetadata.swift in Sources */,
D73E5E612C6A97F4007EB227 /* ImageProcessing.swift in Sources */, D73E5E612C6A97F4007EB227 /* ImageProcessing.swift in Sources */,
D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */, D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */,
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */,
D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */, D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */,
D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */, D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */,
D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */, D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */,
@@ -5699,6 +5743,7 @@
D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */, D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */,
D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */, D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */,
D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */, D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */,
5C4FA7EE2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
D73E5F922C6AA720007EB227 /* QRCodeView.swift in Sources */, D73E5F922C6AA720007EB227 /* QRCodeView.swift in Sources */,
D73E5E742C6A97F4007EB227 /* Lists.swift in Sources */, D73E5E742C6A97F4007EB227 /* Lists.swift in Sources */,
D73E5E752C6A97F4007EB227 /* CoreSVG.swift in Sources */, D73E5E752C6A97F4007EB227 /* CoreSVG.swift in Sources */,
@@ -5716,6 +5761,7 @@
D73E5E802C6A97F4007EB227 /* CredentialHandler.swift in Sources */, D73E5E802C6A97F4007EB227 /* CredentialHandler.swift in Sources */,
D73E5E812C6A97F4007EB227 /* KeyboardVisible.swift in Sources */, D73E5E812C6A97F4007EB227 /* KeyboardVisible.swift in Sources */,
D73E5E832C6A97F4007EB227 /* AVPlayer+Additions.swift in Sources */, D73E5E832C6A97F4007EB227 /* AVPlayer+Additions.swift in Sources */,
5C4FA7FC2DC29C3800CE658C /* FollowPackView.swift in Sources */,
D73E5E842C6A97F4007EB227 /* Zaps+.swift in Sources */, D73E5E842C6A97F4007EB227 /* Zaps+.swift in Sources */,
D73E5E852C6A97F4007EB227 /* WalletConnect+.swift in Sources */, D73E5E852C6A97F4007EB227 /* WalletConnect+.swift in Sources */,
D73E5E862C6A97F4007EB227 /* DamusPurpleNotificationManagement.swift in Sources */, D73E5E862C6A97F4007EB227 /* DamusPurpleNotificationManagement.swift in Sources */,
@@ -5852,6 +5898,7 @@
D73E5EFF2C6A97F4007EB227 /* ZapsView.swift in Sources */, D73E5EFF2C6A97F4007EB227 /* ZapsView.swift in Sources */,
D73E5F002C6A97F4007EB227 /* CustomizeZapView.swift in Sources */, D73E5F002C6A97F4007EB227 /* CustomizeZapView.swift in Sources */,
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */, D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
5C4FA8052DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */, D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */, D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */, D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
@@ -5990,6 +6037,7 @@
D703D7552C670A3700A400EA /* DamusUserDefaults.swift in Sources */, D703D7552C670A3700A400EA /* DamusUserDefaults.swift in Sources */,
D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */, D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */,
D703D7992C670DF900A400EA /* sha256.c in Sources */, D703D7992C670DF900A400EA /* sha256.c in Sources */,
5C4FA8002DC5119300CE658C /* FollowPackPreview.swift in Sources */,
D703D7972C670DED00A400EA /* wasm.c in Sources */, D703D7972C670DED00A400EA /* wasm.c in Sources */,
5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */, 5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */,
D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */, D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */,

View File

@@ -13,6 +13,7 @@ enum FilterState : Int {
case posts = 0 case posts = 0
case posts_and_replies = 1 case posts_and_replies = 1
case conversations = 2 case conversations = 2
case follow_list = 3
func filter(ev: NostrEvent) -> Bool { func filter(ev: NostrEvent) -> Bool {
switch self { switch self {
@@ -22,6 +23,8 @@ enum FilterState : Int {
return true return true
case .conversations: case .conversations:
return true return true
case .follow_list:
return ev.known_kind == .follow_list
} }
} }
} }

View File

@@ -0,0 +1,39 @@
//
// FollowPackEvent.swift
// damus
//
// Created by eric on 4/30/25.
//
import Foundation
struct FollowPackEvent {
let event: NostrEvent
var title: String? = nil
var uuid: String? = nil
var image: URL? = nil
var description: String? = nil
var publicKeys: [Pubkey] = []
static func parse(from ev: NostrEvent) -> FollowPackEvent {
var followlist = FollowPackEvent(event: ev)
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0].string() {
case "title": followlist.title = tag[1].string()
case "d": followlist.uuid = tag[1].string()
case "image": followlist.image = URL(string: tag[1].string())
case "description": followlist.description = tag[1].string()
case "p":
followlist.publicKeys.append(Pubkey(Data(hex: tag[1].string())))
default:
break
}
}
return followlist
}
}

View File

@@ -0,0 +1,77 @@
//
// FollowPackModel.swift
// damus
//
// Created by eric on 6/5/25.
//
import Foundation
class FollowPackModel: ObservableObject {
var events: EventHolder
@Published var loading: Bool = false
let damus_state: DamusState
let subid = UUID().description
let limit: UInt32 = 500
init(damus_state: DamusState) {
self.damus_state = damus_state
self.events = EventHolder(on_queue: { ev in
preload_events(state: damus_state, events: [ev])
})
}
func subscribe(follow_pack_users: [Pubkey]) {
loading = true
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
var filter = NostrFilter(kinds: [.text, .chat])
filter.until = UInt32(Date.now.timeIntervalSince1970)
filter.authors = follow_pack_users
filter.limit = 500
damus_state.nostrNetwork.pool.subscribe(sub_id: subid, filters: [filter], handler: handle_event, to: to_relays)
}
func unsubscribe(to: RelayURL? = nil) {
loading = false
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: to.map { [$0] })
}
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
guard case .nostr_event(let event) = conn_ev else {
return
}
switch event {
case .event(let sub_id, let ev):
guard sub_id == self.subid else {
return
}
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
{
if self.events.insert(ev) {
self.objectWillChange.send()
}
}
case .notice(let msg):
print("follow pack notice: \(msg)")
case .ok:
break
case .eose(let sub_id):
loading = false
if sub_id == self.subid {
unsubscribe(to: relay_id)
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
}
break
case .auth:
break
}
}
}

View File

@@ -227,6 +227,8 @@ class HomeModel: ContactsDelegate {
break break
case .relay_list: case .relay_list:
break // This will be handled by `UserRelayListManager` break // This will be handled by `UserRelayListManager`
case .follow_list:
break
} }
} }

View File

@@ -1,4 +1,3 @@
//
// SearchHomeModel.swift // SearchHomeModel.swift
// damus // damus
// //
@@ -16,6 +15,7 @@ class SearchHomeModel: ObservableObject {
var seen_pubkey: Set<Pubkey> = Set() var seen_pubkey: Set<Pubkey> = Set()
let damus_state: DamusState let damus_state: DamusState
let base_subid = UUID().description let base_subid = UUID().description
let follow_pack_subid = UUID().description
let profiles_subid = UUID().description let profiles_subid = UUID().description
let limit: UInt32 = 500 let limit: UInt32 = 500
//let multiple_events_per_pubkey: Bool = false //let multiple_events_per_pubkey: Bool = false
@@ -42,12 +42,18 @@ class SearchHomeModel: ObservableObject {
func subscribe() { func subscribe() {
loading = true loading = true
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters) let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
var follow_list_filter = NostrFilter(kinds: [.follow_list])
follow_list_filter.until = UInt32(Date.now.timeIntervalSince1970)
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays) damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
damus_state.nostrNetwork.pool.subscribe(sub_id: follow_pack_subid, filters: [follow_list_filter], handler: handle_event, to: to_relays)
} }
func unsubscribe(to: RelayURL? = nil) { func unsubscribe(to: RelayURL? = nil) {
loading = false loading = false
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] }) damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
damus_state.nostrNetwork.pool.unsubscribe(sub_id: follow_pack_subid, to: to.map { [$0] })
} }
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) { func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
@@ -57,7 +63,7 @@ class SearchHomeModel: ObservableObject {
switch event { switch event {
case .event(let sub_id, let ev): case .event(let sub_id, let ev):
guard sub_id == self.base_subid || sub_id == self.profiles_subid else { guard sub_id == self.base_subid || sub_id == self.profiles_subid || sub_id == self.follow_pack_subid else {
return return
} }
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply() if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()

View File

@@ -36,7 +36,7 @@ class SearchModel: ObservableObject {
func subscribe() { func subscribe() {
// since 1 month // since 1 month
search.limit = self.limit search.limit = self.limit
search.kinds = [.text, .like, .longform, .highlight] search.kinds = [.text, .like, .longform, .highlight, .follow_list]
//likes_filter.ids = ref_events.referenced_ids! //likes_filter.ids = ref_events.referenced_ids!

View File

@@ -30,4 +30,5 @@ enum NostrKind: UInt32, Codable {
case nwc_response = 23195 case nwc_response = 23195
case http_auth = 27235 case http_auth = 27235
case status = 30315 case status = 30315
case follow_list = 39089
} }

View File

@@ -49,6 +49,7 @@ enum Route: Hashable {
case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel) case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel)
case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?) case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?)
case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey]) case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey])
case FollowPack(followPack: NostrEvent, model: FollowPackModel, blur_imgs: Bool)
@ViewBuilder @ViewBuilder
func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View { func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View {
@@ -134,6 +135,8 @@ enum Route: Hashable {
NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon) NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon)
case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys): case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys):
NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys) NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
case .FollowPack(let followPack, let followPackModel, let blur_imgs):
FollowPackView(state: damusState, ev: followPack, model: followPackModel, blur_imgs: blur_imgs)
} }
} }
@@ -244,6 +247,9 @@ enum Route: Hashable {
case .NIP05DomainPubkeys(let domain, _, _): case .NIP05DomainPubkeys(let domain, _, _):
hasher.combine("nip05DomainPubkeys") hasher.combine("nip05DomainPubkeys")
hasher.combine(domain) hasher.combine(domain)
case .FollowPack(let followPack, let followPackModel, let blur_imgs):
hasher.combine("followPack")
hasher.combine(followPack.id)
} }
} }
} }

View File

@@ -0,0 +1,242 @@
//
// FollowPackPreview.swift
// damus
//
// Created by eric on 4/30/25.
//
import SwiftUI
import Kingfisher
struct FollowPackUsers: View {
let state: DamusState
var publicKeys: [Pubkey]
var body: some View {
HStack(alignment: .center) {
if !publicKeys.isEmpty {
CondensedProfilePicturesView(state: state, pubkeys: publicKeys, maxPictures: 5)
}
let followPackUserCount = publicKeys.count
let nounString = pluralizedString(key: "follow_pack_user_count", count: followPackUserCount)
let nounText = Text(verbatim: nounString).font(.subheadline).foregroundColor(.gray)
Text("\(Text(verbatim: followPackUserCount.formatted()).font(.subheadline.weight(.medium))) \(nounText)", comment: "Sentence composed of 2 variables to describe how many people are in the follow pack. In source English, the first variable is the number of users, and the second variable is 'user' or 'users'.")
}
}
}
struct FollowPackBannerImage: View {
let state: DamusState
let options: EventViewOptions
var image: URL? = nil
var preview: Bool
@State var blur_imgs: Bool
func Placeholder(url: URL, preview: Bool) -> some View {
Group {
if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash)
.resizable()
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
} else {
DamusColors.adaptableWhite
}
}
}
func titleImage(url: URL, preview: Bool) -> some View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.background {
Placeholder(url: url, preview: preview)
}
.aspectRatio(contentMode: .fill)
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
.kfClickable()
.cornerRadius(1)
}
var body: some View {
if let url = image {
if (self.options.contains(.no_media)) {
EmptyView()
} else if !blur_imgs {
titleImage(url: url, preview: preview)
} else {
ZStack {
titleImage(url: url, preview: preview)
BlurOverlayView(blur_images: $blur_imgs, artifacts: nil, size: nil, damus_state: nil, parentView: .longFormView)
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
}
}
} else {
Text(NSLocalizedString("No cover image", comment: "Text letting user know there is no cover image."))
.foregroundColor(.gray)
.frame(width: 350, height: 180)
Divider()
}
}
}
struct FollowPackPreviewBody: View {
let state: DamusState
let event: FollowPackEvent
let options: EventViewOptions
let header: Bool
@State var blur_imgs: Bool
@ObservedObject var artifacts: NoteArtifactsModel
init(state: DamusState, ev: FollowPackEvent, options: EventViewOptions, header: Bool, blur_imgs: Bool) {
self.state = state
self.event = ev
self.options = options
self.header = header
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
}
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self.options = options
self.header = header
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
}
var body: some View {
Group {
if options.contains(.wide) {
Main.padding(.horizontal)
} else {
Main
}
}
}
var Main: some View {
VStack(alignment: .leading, spacing: 10) {
if state.settings.media_previews {
FollowPackBannerImage(state: state, options: options, image: event.image, preview: true, blur_imgs: blur_imgs)
}
Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of follow list event if it is untitled."))
.font(header ? .title : .headline)
.padding(.horizontal, 10)
.padding(.top, 5)
if let description = event.description {
Text(description)
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
} else {
Text("")
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
HStack(alignment: .center) {
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
}
let profile_txn = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName {
case .one(let one):
Text(one)
.font(.subheadline).foregroundColor(.gray)
case .both(username: let username, displayName: let displayName):
HStack(spacing: 6) {
Text(verbatim: displayName)
.font(.subheadline).foregroundColor(.gray)
Text(verbatim: "@\(username)")
.font(.subheadline).foregroundColor(.gray)
}
}
}
.padding(.horizontal, 10)
FollowPackUsers(state: state, publicKeys: event.publicKeys)
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
.frame(width: 350, height: state.settings.media_previews ? 330 : 150, alignment: .leading)
.background(DamusColors.neutral3)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral1, lineWidth: 1)
)
.padding(.top, 10)
}
}
struct FollowPackPreview: View {
let state: DamusState
let event: FollowPackEvent
let options: EventViewOptions
@State var blur_imgs: Bool
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self.options = options.union(.no_mentions)
self.blur_imgs = blur_imgs
}
var body: some View {
FollowPackPreviewBody(state: state, ev: event, options: options, header: false, blur_imgs: blur_imgs)
}
}
let test_follow_list_event = FollowPackEvent.parse(from: NostrEvent(
content: "",
keypair: test_keypair,
kind: NostrKind.longform.rawValue,
tags: [
["title", "DAMUSES"],
["description", "Damus Team"],
["published_at", "1685638715"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"],
["p", "17538dc2a62769d09443f18c37cbe358fab5bbf981173542aa7c5ff171ed77c4"],
["p", "520830c334a3f79f88cac934580d26f91a7832c6b21fb9625690ea2ed81b5626"],
["p", "2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1"],
["p", "bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91"],
["p", "e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7"],
["p", "b88c7f007bbf3bc2fcaeff9e513f186bab33782c0baa6a6cc12add78b9110ba3"],
["p", "4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"],
["image", "https://damus.io/img/logo.png"],
])!
)
struct FollowPackPreview_Previews: PreviewProvider {
static var previews: some View {
VStack {
FollowPackPreview(state: test_damus_state, ev: test_follow_list_event.event, options: [], blur_imgs: false)
}
.frame(height: 400)
}
}

View File

@@ -0,0 +1,135 @@
//
// FollowPackTimeline.swift
// damus
//
// Created by eric on 5/6/25.
//
import SwiftUI
struct FollowPackTimelineView<Content: View>: View {
@ObservedObject var events: EventHolder
@Binding var loading: Bool
let damus: DamusState
let show_friend_icon: Bool
let filter: (NostrEvent) -> Bool
let content: Content?
let apply_mute_rules: Bool
init(events: EventHolder, loading: Binding<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.content = content?()
}
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.content = content?()
}
var body: some View {
MainContent
}
var MainContent: some View {
ScrollViewReader { scroller in
ScrollView(.horizontal) {
if let content {
content
}
Color.clear
.id("startblock")
.frame(height: 0)
FollowPackInnerView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.disabled(loading)
.background {
GeometryReader { proxy -> Color in
handle_scroll_queue(proxy, queue: self.events)
return Color.clear
}
}
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { () in
events.flush()
self.events.should_queue = false
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
}
}
.onAppear {
events.flush()
}
}
}
struct FollowPackInnerView: View {
@ObservedObject var events: EventHolder
let state: DamusState
let filter: (NostrEvent) -> Bool
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
self.events = events
self.state = damus
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
}
var event_options: EventViewOptions {
if self.state.settings.truncate_timeline_text {
return [.wide, .truncate_content]
}
return [.wide]
}
var body: some View {
LazyHStack(spacing: 0) {
let events = self.events.events
if events.isEmpty {
EmptyTimelineView()
} else {
let evs = events.filter(filter)
let indexed = Array(zip(evs, 0...))
ForEach(indexed, id: \.0.id) { tup in
let ev = tup.0
let ind = tup.1
let blur_imgs = should_blur_images(settings: state.settings, contacts: state.contacts, ev: ev, our_pubkey: state.pubkey)
if ev.kind == NostrKind.follow_list.rawValue {
FollowPackPreview(state: state, ev: ev, options: event_options, blur_imgs: blur_imgs)
.onTapGesture {
state.nav.push(route: Route.FollowPack(followPack: ev, model: FollowPackModel(damus_state: state), blur_imgs: blur_imgs))
}
.padding(.top, 7)
.onAppear {
let to_preload =
Array([indexed[safe: ind+1]?.0,
indexed[safe: ind+2]?.0,
indexed[safe: ind+3]?.0,
indexed[safe: ind+4]?.0,
indexed[safe: ind+5]?.0
].compactMap({ $0 }))
preload_events(state: state, events: to_preload)
}
}
}
}
}
.padding(.bottom)
}
}

View File

@@ -0,0 +1,176 @@
//
// FollowPackView.swift
// damus
//
// Created by eric on 4/30/25.
//
import SwiftUI
import Kingfisher
struct FollowPackView: View {
let state: DamusState
let event: FollowPackEvent
@StateObject var model: FollowPackModel
@State var blur_imgs: Bool
@Environment(\.colorScheme) var colorScheme
@ObservedObject var artifacts: NoteArtifactsModel
init(state: DamusState, ev: FollowPackEvent, model: FollowPackModel, blur_imgs: Bool) {
self.state = state
self.event = ev
self._model = StateObject(wrappedValue: model)
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
}
init(state: DamusState, ev: NostrEvent, model: FollowPackModel, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self._model = StateObject(wrappedValue: model)
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
}
func content_filter(_ pubkeys: [Pubkey]) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: self.state)
filters.append({ pubkeys.contains($0.pubkey) })
return ContentFilters(filters: filters).filter
}
enum FollowPackTabSelection: Int {
case people = 0
case posts = 1
}
@State var tab_selection: FollowPackTabSelection = .people
var body: some View {
ZStack {
ScrollView {
FollowPackHeader
FollowPackTabs
}
}
.onAppear {
if model.events.events.isEmpty {
model.subscribe(follow_pack_users: event.publicKeys)
}
}
.onDisappear {
model.unsubscribe()
}
}
var tabs: [(String, FollowPackTabSelection)] {
let tabs = [
(NSLocalizedString("People", comment: "Label for filter for seeing the people in this follow pack."), FollowPackTabSelection.people),
(NSLocalizedString("Posts", comment: "Label for filter for seeing the posts from the people in this follow pack."), FollowPackTabSelection.posts)
]
return tabs
}
var FollowPackTabs: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
CustomPicker(tabs: tabs, selection: $tab_selection)
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
if tab_selection == FollowPackTabSelection.people {
LazyVStack(alignment: .leading) {
ForEach(event.publicKeys.reversed(), id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: state)
}
}
.padding()
.padding(.bottom, 50)
.tag(FollowPackTabSelection.people)
.id(FollowPackTabSelection.people)
}
if tab_selection == FollowPackTabSelection.posts {
InnerTimelineView(events: model.events, damus: state, filter: content_filter(event.publicKeys))
}
}
.onAppear() {
model.subscribe(follow_pack_users: event.publicKeys)
}
.onDisappear {
model.unsubscribe()
}
}
var FollowPackHeader: some View {
VStack(alignment: .leading, spacing: 10) {
if state.settings.media_previews {
FollowPackBannerImage(state: state, options: EventViewOptions(), image: event.image, preview: false, blur_imgs: blur_imgs)
}
Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of follow list event if it is untitled."))
.font(.title)
.padding(.horizontal, 10)
.padding(.top, 5)
if let description = event.description {
Text(description)
.font(.body)
.foregroundColor(.gray)
.padding(.horizontal, 10)
} else {
EmptyView()
}
HStack(alignment: .center) {
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
}
let profile_txn = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName {
case .one(let one):
Text(NSLocalizedString("Created by \(one)", comment: "Lets the user know who created this follow pack."))
.font(.subheadline).foregroundColor(.gray)
case .both(username: let username, displayName: let displayName):
HStack(spacing: 6) {
Text(NSLocalizedString("Created by \(displayName)", comment: "Lets the user know who created this follow pack."))
.font(.subheadline).foregroundColor(.gray)
Text(verbatim: "@\(username)")
.font(.subheadline).foregroundColor(.gray)
}
}
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
HStack(alignment: .center) {
FollowPackUsers(state: state, publicKeys: event.publicKeys)
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
}
}
struct FollowPackView_Previews: PreviewProvider {
static var previews: some View {
VStack {
FollowPackView(state: test_damus_state, ev: test_follow_list_event, model: FollowPackModel(damus_state: test_damus_state), blur_imgs: false)
}
.frame(height: 400)
}
}

View File

@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
case .zap, .zap_request: case .zap, .zap_request:
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found } guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
return .loaded(route: Route.Zaps(target: zap.target)) return .loaded(route: Route.Zaps(target: zap.target))
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list: case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list:
return .unknown_or_unsupported_kind return .unknown_or_unsupported_kind
} }
case .naddr(let naddr): case .naddr(let naddr):

View File

@@ -123,7 +123,7 @@ struct ProfileView: View {
var filters = ContentFilters.defaults(damus_state: damus_state) var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter) filters.append(fstate.filter)
switch fstate { switch fstate {
case .posts, .posts_and_replies: case .posts, .posts_and_replies, .follow_list:
filters.append({ profile.pubkey == $0.pubkey }) filters.append({ profile.pubkey == $0.pubkey })
case .conversations: case .conversations:
filters.append({ profile.conversation_events.contains($0.id) } ) filters.append({ profile.conversation_events.contains($0.id) } )

View File

@@ -15,8 +15,9 @@ struct SearchHomeView: View {
@State var search: String = "" @State var search: String = ""
@FocusState private var isFocused: Bool @FocusState private var isFocused: Bool
var content_filter: (NostrEvent) -> Bool { func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
let filters = ContentFilters.defaults(damus_state: self.damus_state) var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter)
return ContentFilters(filters: filters).filter return ContentFilters(filters: filters).filter
} }
@@ -52,21 +53,20 @@ struct SearchHomeView: View {
loading: $model.loading, loading: $model.loading,
damus: damus_state, damus: damus_state,
show_friend_icon: true, show_friend_icon: true,
filter: { ev in filter:content_filter(FilterState.posts),
if !content_filter(ev) {
return false
}
let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
if event_muted {
return false
}
return true
},
content: { content: {
AnyView(VStack { AnyView(VStack(alignment: .leading) {
SuggestedHashtagsView(damus_state: damus_state, max_items: 5, events: model.events) HStack {
Image(systemName: "sparkles")
.foregroundStyle(PinkGradient)
Text("Follow Packs", comment: "A label indicating that the items below it are follow packs")
.foregroundStyle(PinkGradient)
}
.padding(.top)
.padding(.horizontal)
FollowPackTimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true,filter:content_filter(FilterState.follow_list)
).padding(.bottom)
Divider() Divider()
.frame(height: 1) .frame(height: 1)

View File

@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>user</string>
<key>other</key>
<string>users</string>
</dict>
</dict>
<key>followed_by_three_and_others</key> <key>followed_by_three_and_others</key>
<dict> <dict>
<key>NSStringLocalizedFormatKey</key> <key>NSStringLocalizedFormatKey</key>

View File

@@ -15,6 +15,7 @@ final class LocalizationUtilTests: XCTestCase {
// Test cases of the localization string key, and the expected en-US strings for a count of 0, 1, and 2. // Test cases of the localization string key, and the expected en-US strings for a count of 0, 1, and 2.
let keys = [ let keys = [
["follow_pack_user_count", "users", "user", "users"],
["followers_count", "Followers", "Follower", "Followers"], ["followers_count", "Followers", "Follower", "Followers"],
["following_count", "Following", "Following", "Following"], ["following_count", "Following", "Following", "Following"],
["hellthread_notifications_disabled", "Hide notifications that tag more than 0 profiles", "Hide notifications that tag more than 1 profile", "Hide notifications that tag more than 2 profiles"], ["hellthread_notifications_disabled", "Hide notifications that tag more than 0 profiles", "Hide notifications that tag more than 1 profile", "Hide notifications that tag more than 2 profiles"],

View File

@@ -368,7 +368,7 @@ class NdbNote: Codable, Equatable, Hashable {
// Extension to make NdbNote compatible with NostrEvent's original API // Extension to make NdbNote compatible with NostrEvent's original API
extension NdbNote { extension NdbNote {
var is_textlike: Bool { var is_textlike: Bool {
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802 return kind == 1 || kind == 42 || kind == 30023 || kind == 9802 || kind == 39089
} }
var is_quote_repost: NoteId? { var is_quote_repost: NoteId? {