Add Timeline switcher button in PostingTimelineView. Switch between your following or NIP-81 favorites. User can favorite a user via ProfileActionSheetView or ProfileView.
Closes: https://github.com/damus-io/damus/issues/2438 Changelog-Added: Add Timeline switcher button for NIP-81-favorites Signed-off-by: Askeew <askeew@hotmail.com>
This commit is contained in:
committed by
Daniel D’Aquino
parent
6605c5e583
commit
61f695b7c6
@@ -8,6 +8,9 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */; };
|
||||
2710433D2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2710433C2E6BFE2A0005C3B0 /* PostingTimelineSwitcherView.swift */; };
|
||||
2710433E2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2710433C2E6BFE2A0005C3B0 /* PostingTimelineSwitcherView.swift */; };
|
||||
2710433F2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2710433C2E6BFE2A0005C3B0 /* PostingTimelineSwitcherView.swift */; };
|
||||
3165648B295B70D500C64604 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3165648A295B70D500C64604 /* LinkView.swift */; };
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||
@@ -1029,6 +1032,19 @@
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
||||
D5C1AFBF2E5DF7E60092F72F /* ContactCardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFBE2E5DF7E60092F72F /* ContactCardManager.swift */; };
|
||||
D5C1AFC02E5DF7E60092F72F /* ContactCardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFBE2E5DF7E60092F72F /* ContactCardManager.swift */; };
|
||||
D5C1AFC12E5DF7E60092F72F /* ContactCardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFBE2E5DF7E60092F72F /* ContactCardManager.swift */; };
|
||||
D5C1AFC42E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC32E5DFF700092F72F /* ContactCardManagerMock.swift */; };
|
||||
D5C1AFC52E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC32E5DFF700092F72F /* ContactCardManagerMock.swift */; };
|
||||
D5C1AFC62E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC32E5DFF700092F72F /* ContactCardManagerMock.swift */; };
|
||||
D5C1AFC82E5E00690092F72F /* ContactCardManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC72E5E00690092F72F /* ContactCardManagerTests.swift */; };
|
||||
D5C1AFCA2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC92E5EE12B0092F72F /* ContactCardNotify.swift */; };
|
||||
D5C1AFCB2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC92E5EE12B0092F72F /* ContactCardNotify.swift */; };
|
||||
D5C1AFCC2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFC92E5EE12B0092F72F /* ContactCardNotify.swift */; };
|
||||
D5C1AFD32E5EE2820092F72F /* FavoriteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFD12E5EE2820092F72F /* FavoriteButtonView.swift */; };
|
||||
D5C1AFD42E5EE2820092F72F /* FavoriteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFD12E5EE2820092F72F /* FavoriteButtonView.swift */; };
|
||||
D5C1AFD52E5EE2820092F72F /* FavoriteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C1AFD12E5EE2820092F72F /* FavoriteButtonView.swift */; };
|
||||
D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D703D7182C66E47100A400EA /* UniformTypeIdentifiers.framework */; };
|
||||
D703D71C2C66E47100A400EA /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D703D71B2C66E47100A400EA /* Media.xcassets */; };
|
||||
D703D71E2C66E47100A400EA /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D703D71D2C66E47100A400EA /* ActionViewController.swift */; };
|
||||
@@ -1879,6 +1895,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrFilter+Hashable.swift"; sourceTree = "<group>"; };
|
||||
2710433C2E6BFE2A0005C3B0 /* PostingTimelineSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineSwitcherView.swift; sourceTree = "<group>"; };
|
||||
3165648A295B70D500C64604 /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; };
|
||||
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = "<group>"; };
|
||||
3169CAEC294FCCFC00EE4006 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = damus/Shared/Utilities/Constants.swift; sourceTree = SOURCE_ROOT; };
|
||||
@@ -2581,6 +2598,11 @@
|
||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
|
||||
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
|
||||
D5C1AFBE2E5DF7E60092F72F /* ContactCardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCardManager.swift; sourceTree = "<group>"; };
|
||||
D5C1AFC32E5DFF700092F72F /* ContactCardManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCardManagerMock.swift; sourceTree = "<group>"; };
|
||||
D5C1AFC72E5E00690092F72F /* ContactCardManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCardManagerTests.swift; sourceTree = "<group>"; };
|
||||
D5C1AFC92E5EE12B0092F72F /* ContactCardNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCardNotify.swift; sourceTree = "<group>"; };
|
||||
D5C1AFD12E5EE2820092F72F /* FavoriteButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButtonView.swift; sourceTree = "<group>"; };
|
||||
D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HighlighterActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D703D7182C66E47100A400EA /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
D703D71B2C66E47100A400EA /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
||||
@@ -3206,6 +3228,7 @@
|
||||
4CA3529C2A76AE47003BB08B /* Notify */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5C1AFC92E5EE12B0092F72F /* ContactCardNotify.swift */,
|
||||
D706C5B62D602A050027C627 /* QueueableNotify.swift */,
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */,
|
||||
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
|
||||
@@ -3986,6 +4009,7 @@
|
||||
5C78A7792E22FDFE00CF177D /* Features */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5C1AFC22E5DFF040092F72F /* ContactCard */,
|
||||
5C78A7BC2E304D7400CF177D /* Translations */,
|
||||
5C78A7B52E3046F400CF177D /* NIP05 */,
|
||||
5C78A7AA2E30428D00CF177D /* Actions */,
|
||||
@@ -4534,6 +4558,7 @@
|
||||
5C78A7A92E30419B00CF177D /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2710433C2E6BFE2A0005C3B0 /* PostingTimelineSwitcherView.swift */,
|
||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
|
||||
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
@@ -4850,6 +4875,24 @@
|
||||
path = Camera;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D5C1AFC22E5DFF040092F72F /* ContactCard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5C1AFD22E5EE2820092F72F /* Views */,
|
||||
D5C1AFBE2E5DF7E60092F72F /* ContactCardManager.swift */,
|
||||
D5C1AFC32E5DFF700092F72F /* ContactCardManagerMock.swift */,
|
||||
);
|
||||
path = ContactCard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D5C1AFD22E5EE2820092F72F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5C1AFD12E5EE2820092F72F /* FavoriteButtonView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D703D71A2C66E47100A400EA /* highlighter action extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -5028,6 +5071,7 @@
|
||||
F944F56C29EA9CB20067B3BF /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5C1AFC72E5E00690092F72F /* ContactCardManagerTests.swift */,
|
||||
F944F56D29EA9CCC0067B3BF /* DamusParseContentTests.swift */,
|
||||
75AD872A2AA23A460085EF2C /* Block+Tests.swift */,
|
||||
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */,
|
||||
@@ -5438,6 +5482,7 @@
|
||||
D798D22C2B086C7400234419 /* NostrEvent+.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
|
||||
D5C1AFBF2E5DF7E60092F72F /* ContactCardManager.swift in Sources */,
|
||||
4CC6AA752CAB688500989CEF /* str.c in Sources */,
|
||||
4CC6AA762CAB688500989CEF /* tal.c in Sources */,
|
||||
4CC6AA782CAB688500989CEF /* mem.c in Sources */,
|
||||
@@ -5454,6 +5499,7 @@
|
||||
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
2710433D2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */,
|
||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||
4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */,
|
||||
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */,
|
||||
@@ -5531,6 +5577,7 @@
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
||||
D5C1AFC62E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */,
|
||||
4C4DD3DB2A6CA7E8005B4E85 /* ContentParsing.swift in Sources */,
|
||||
F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */,
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||
@@ -5829,6 +5876,8 @@
|
||||
D76BE18D2E0CF3DA004AD0C6 /* Interests.swift in Sources */,
|
||||
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
|
||||
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
|
||||
D5C1AFD42E5EE2820092F72F /* FavoriteButtonView.swift in Sources */,
|
||||
D5C1AFCA2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */,
|
||||
4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */,
|
||||
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
|
||||
7527271E2A93FF0100214108 /* Block.swift in Sources */,
|
||||
@@ -5950,6 +5999,7 @@
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
|
||||
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
||||
D5C1AFC82E5E00690092F72F /* ContactCardManagerTests.swift in Sources */,
|
||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */,
|
||||
4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */,
|
||||
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
|
||||
@@ -6046,6 +6096,7 @@
|
||||
82D6FACA2CD99F7900C925F4 /* NostrScript.swift in Sources */,
|
||||
82D6FACB2CD99F7900C925F4 /* nostrscript.c in Sources */,
|
||||
D73C7EDA2DE51690001F9392 /* OnboardingSuggestionsView.swift in Sources */,
|
||||
D5C1AFC52E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */,
|
||||
82D6FADE2CD99F7900C925F4 /* ThreadReply.swift in Sources */,
|
||||
82D6FADF2CD99F7900C925F4 /* AttachedWalletNotify.swift in Sources */,
|
||||
82D6FAE02CD99F7900C925F4 /* DisplayTabBarNotify.swift in Sources */,
|
||||
@@ -6105,6 +6156,7 @@
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
82D6FB0C2CD99F7900C925F4 /* GoldSupportGradient.swift in Sources */,
|
||||
82D6FB0D2CD99F7900C925F4 /* PinkGradient.swift in Sources */,
|
||||
D5C1AFD32E5EE2820092F72F /* FavoriteButtonView.swift in Sources */,
|
||||
82D6FB0E2CD99F7900C925F4 /* GrayGradient.swift in Sources */,
|
||||
82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */,
|
||||
82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */,
|
||||
@@ -6247,6 +6299,7 @@
|
||||
82D6FB912CD99F7900C925F4 /* UserSettingsStore.swift in Sources */,
|
||||
82D6FB922CD99F7900C925F4 /* Wallet.swift in Sources */,
|
||||
82D6FB932CD99F7900C925F4 /* Report.swift in Sources */,
|
||||
2710433F2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */,
|
||||
82D6FB942CD99F7900C925F4 /* LibreTranslateServer.swift in Sources */,
|
||||
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||
82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
|
||||
@@ -6418,6 +6471,7 @@
|
||||
82D6FC312CD99F7900C925F4 /* ProxyView.swift in Sources */,
|
||||
82D6FC322CD99F7900C925F4 /* SelectedEventView.swift in Sources */,
|
||||
82D6FC332CD99F7900C925F4 /* EventBody.swift in Sources */,
|
||||
D5C1AFC02E5DF7E60092F72F /* ContactCardManager.swift in Sources */,
|
||||
82D6FC342CD99F7900C925F4 /* BuilderEventView.swift in Sources */,
|
||||
82D6FC352CD99F7900C925F4 /* EventProfile.swift in Sources */,
|
||||
82D6FC362CD99F7900C925F4 /* EventMenu.swift in Sources */,
|
||||
@@ -6493,6 +6547,7 @@
|
||||
82D6FC772CD99F7900C925F4 /* SuggestedHashtagsView.swift in Sources */,
|
||||
82D6FC782CD99F7900C925F4 /* ProfileActionSheetView.swift in Sources */,
|
||||
82D6FC792CD99F7900C925F4 /* damusApp.swift in Sources */,
|
||||
D5C1AFCC2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */,
|
||||
82D6FC7A2CD99F7900C925F4 /* ContentView.swift in Sources */,
|
||||
82D6FC7B2CD99F7900C925F4 /* TestData.swift in Sources */,
|
||||
82D6FC7C2CD99F7900C925F4 /* ContentParsing.swift in Sources */,
|
||||
@@ -6517,6 +6572,7 @@
|
||||
4C3624732D5EA1BE00DD066E /* nostrdb.c in Sources */,
|
||||
4C3624602D5E9EB800DD066E /* NdbProfile.swift in Sources */,
|
||||
4C36245F2D5E9B5F00DD066E /* NdbBlock.swift in Sources */,
|
||||
D5C1AFD52E5EE2820092F72F /* FavoriteButtonView.swift in Sources */,
|
||||
D73E5E202C6A97F4007EB227 /* AttachedWalletNotify.swift in Sources */,
|
||||
D73E5E212C6A97F4007EB227 /* DisplayTabBarNotify.swift in Sources */,
|
||||
D73E5E222C6A97F4007EB227 /* BroadcastNotify.swift in Sources */,
|
||||
@@ -6577,6 +6633,7 @@
|
||||
D73E5E582C6A97F4007EB227 /* ThiccDivider.swift in Sources */,
|
||||
D73E5E592C6A97F4007EB227 /* IconLabel.swift in Sources */,
|
||||
D73E5E5A2C6A97F4007EB227 /* TruncatedText.swift in Sources */,
|
||||
D5C1AFC42E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */,
|
||||
D73E5E5B2C6A97F4007EB227 /* SupporterBadge.swift in Sources */,
|
||||
D73E5E5C2C6A97F4007EB227 /* GradientButtonStyle.swift in Sources */,
|
||||
D73E5E5D2C6A97F4007EB227 /* NeutralButtonStyle.swift in Sources */,
|
||||
@@ -6706,6 +6763,7 @@
|
||||
D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */,
|
||||
D73E5EBA2C6A97F4007EB227 /* NostrFilter.swift in Sources */,
|
||||
D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */,
|
||||
D5C1AFCB2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */,
|
||||
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
|
||||
D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */,
|
||||
D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */,
|
||||
@@ -6764,6 +6822,7 @@
|
||||
D73E5EF62C6A97F4007EB227 /* SearchingEventView.swift in Sources */,
|
||||
D73E5EF72C6A97F4007EB227 /* PullDownSearch.swift in Sources */,
|
||||
D73E5EF82C6A97F4007EB227 /* NotificationsView.swift in Sources */,
|
||||
D5C1AFC12E5DF7E60092F72F /* ContactCardManager.swift in Sources */,
|
||||
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
|
||||
D73E5EF92C6A97F4007EB227 /* EventGroupView.swift in Sources */,
|
||||
D73E5EFA2C6A97F4007EB227 /* NotificationItemView.swift in Sources */,
|
||||
@@ -6965,6 +7024,7 @@
|
||||
D703D7A42C670E3C00A400EA /* midl.c in Sources */,
|
||||
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
2710433E2E6BFE340005C3B0 /* PostingTimelineSwitcherView.swift in Sources */,
|
||||
D703D78B2C670C9500A400EA /* MakeZapRequest.swift in Sources */,
|
||||
D703D7862C670C6500A400EA /* NewUnmutesNotify.swift in Sources */,
|
||||
D703D7662C670AFC00A400EA /* AsciiCharacter.swift in Sources */,
|
||||
|
||||
@@ -447,6 +447,9 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.present_full_screen_item)) { item in
|
||||
self.active_full_screen_item = item
|
||||
}
|
||||
.onReceive(handle_notify(.favoriteUpdated)) { _ in
|
||||
home.refresh_home_filters()
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { zap_ev in
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
@@ -681,6 +684,7 @@ struct ContentView: View {
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
contactCards: ContactCardManager(),
|
||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
|
||||
@@ -872,4 +872,31 @@ extension NostrEvent {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
var debugDescription: String {
|
||||
var output = "🔍 NostrEvent Debug Info\n"
|
||||
output += "═══════════════════════════\n"
|
||||
output += "📝 ID: \(id)\n"
|
||||
output += "👤 Pubkey: \(pubkey)\n"
|
||||
output += "📅 Created: \(Date(timeIntervalSince1970: TimeInterval(created_at))) (\(created_at))\n"
|
||||
output += "🏷️ Kind: \(kind) (\(String(describing: known_kind))\n"
|
||||
output += "✍️ Signature: \(sig)\n"
|
||||
output += "📄 Content (\(content.count) chars):\n"
|
||||
output += " \"\(content.prefix(100))\(content.count > 100 ? "..." : "")\"\n"
|
||||
|
||||
output += "\n🏷️ Tags (\(tags.count) total):\n"
|
||||
for (index, tag) in tags.enumerated() {
|
||||
output += " [\(index)]: ["
|
||||
for (tagIndex, tagElem) in tag.enumerated() {
|
||||
if tagIndex > 0 { output += ", " }
|
||||
output += "\"\(tagElem.string())\""
|
||||
}
|
||||
output += "]\n"
|
||||
}
|
||||
|
||||
output += "═══════════════════════════\n"
|
||||
return output
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -31,5 +31,6 @@ enum NostrKind: UInt32, Codable {
|
||||
case nwc_response = 23195
|
||||
case http_auth = 27235
|
||||
case status = 30315
|
||||
case contact_card = 30_382
|
||||
case follow_list = 39089
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class DamusState: HeadlessDamusState {
|
||||
let boosts: EventCounter
|
||||
let quote_reposts: EventCounter
|
||||
let contacts: Contacts
|
||||
let contactCards: ContactCard
|
||||
let mutelist_manager: MutelistManager
|
||||
let profiles: Profiles
|
||||
let dms: DirectMessagesModel
|
||||
@@ -39,11 +40,12 @@ class DamusState: HeadlessDamusState {
|
||||
let favicon_cache: FaviconCache
|
||||
private(set) var nostrNetwork: NostrNetworkManager
|
||||
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, contactCards: ContactCard, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.contacts = contacts
|
||||
self.contactCards = contactCards
|
||||
self.mutelist_manager = mutelist_manager
|
||||
self.profiles = profiles
|
||||
self.dms = dms
|
||||
@@ -109,6 +111,7 @@ class DamusState: HeadlessDamusState {
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
contactCards: ContactCardManager(),
|
||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
@@ -178,6 +181,7 @@ class DamusState: HeadlessDamusState {
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
contacts: Contacts(our_pubkey: empty_pub),
|
||||
contactCards: ContactCardManagerMock(),
|
||||
mutelist_manager: MutelistManager(user_keypair: kp),
|
||||
profiles: Profiles(ndb: .empty),
|
||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||
|
||||
128
damus/Features/ContactCard/ContactCardManager.swift
Normal file
128
damus/Features/ContactCard/ContactCardManager.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Manages user's favorites using NIP-81 contact cards
|
||||
class ContactCardManager: ContactCard {
|
||||
private(set) var favorites: Set<Pubkey> = []
|
||||
private var latestContactCardEvents: [Pubkey: NostrEvent] = [:]
|
||||
public static let FAVORITE_TAG = "favorite"
|
||||
public static let CONTACT_SET = "n"
|
||||
public static let TARGET_PUBLIC_KEY = "d"
|
||||
|
||||
public init() {}
|
||||
|
||||
func isFavorite(_ pubkey: Pubkey) -> Bool {
|
||||
favorites.contains(pubkey)
|
||||
}
|
||||
|
||||
func toggleFavorite(_ pubkey: Pubkey, postbox: PostBox, keyPair: FullKeypair?) {
|
||||
if favorites.contains(pubkey) {
|
||||
favorites.remove(pubkey)
|
||||
handleFavorite(target: pubkey, favorite: false, postbox: postbox, keypair: keyPair)
|
||||
} else {
|
||||
favorites.insert(pubkey)
|
||||
handleFavorite(target: pubkey, favorite: true, postbox: postbox, keypair: keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEvent(_ ev: NostrEvent, pubkey: Pubkey) {
|
||||
guard let kind = ev.known_kind, kind == .contact_card else {
|
||||
return
|
||||
}
|
||||
// we only care about our contact cards
|
||||
guard ev.pubkey == pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
var targetPubkey: Pubkey?
|
||||
var isFavorite = false
|
||||
for tag in ev.tags {
|
||||
guard tag.count >= 2 else { continue }
|
||||
let tagType = tag[0].string()
|
||||
let tagValue = tag[1].string()
|
||||
if tagType == Self.TARGET_PUBLIC_KEY {
|
||||
targetPubkey = Pubkey(hex: tagValue)
|
||||
} else if tagType == Self.CONTACT_SET && tagValue == Self.FAVORITE_TAG {
|
||||
isFavorite = true
|
||||
}
|
||||
}
|
||||
|
||||
guard let targetPubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only process if this event is new
|
||||
if let existingEvent = latestContactCardEvents[targetPubkey] {
|
||||
guard ev.created_at > existingEvent.created_at else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if isFavorite {
|
||||
favorites.insert(targetPubkey)
|
||||
} else {
|
||||
favorites.remove(targetPubkey)
|
||||
}
|
||||
|
||||
latestContactCardEvents[targetPubkey] = ev
|
||||
notify(.favoriteUpdated())
|
||||
}
|
||||
|
||||
var filter: (NostrEvent) -> Bool {
|
||||
{ [weak self] ev in
|
||||
guard let self else { return false }
|
||||
return self.isFavorite(ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
private func createFavoriteContactCard(keypair: FullKeypair, target: Pubkey) -> NostrEvent? {
|
||||
let kind = NostrKind.contact_card.rawValue
|
||||
let tags = [
|
||||
[Self.TARGET_PUBLIC_KEY, target.hex()],
|
||||
[Self.CONTACT_SET, Self.FAVORITE_TAG]
|
||||
]
|
||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||
}
|
||||
|
||||
private func createUnfavoriteContactCard(keypair: FullKeypair, target: Pubkey) -> NostrEvent? {
|
||||
let kind = NostrKind.contact_card.rawValue
|
||||
let tags = [
|
||||
[Self.TARGET_PUBLIC_KEY, target.hex()]
|
||||
]
|
||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||
}
|
||||
|
||||
private func handleFavorite(target: Pubkey, favorite: Bool, postbox: PostBox, keypair: FullKeypair?) {
|
||||
guard let keypair else {
|
||||
return
|
||||
}
|
||||
let ev: NostrEvent?
|
||||
if favorite {
|
||||
ev = createFavoriteContactCard(keypair: keypair, target: target)
|
||||
} else {
|
||||
ev = createUnfavoriteContactCard(keypair: keypair, target: target)
|
||||
}
|
||||
|
||||
guard let ev else {
|
||||
return
|
||||
}
|
||||
|
||||
if favorite {
|
||||
favorites.insert(target)
|
||||
} else {
|
||||
favorites.remove(target)
|
||||
}
|
||||
|
||||
postbox.send(ev)
|
||||
latestContactCardEvents[target] = ev
|
||||
notify(.favoriteUpdated())
|
||||
}
|
||||
}
|
||||
|
||||
protocol ContactCard {
|
||||
func isFavorite(_ pubkey: Pubkey) -> Bool
|
||||
func toggleFavorite(_ pubkey: Pubkey, postbox: PostBox, keyPair: FullKeypair?)
|
||||
func loadEvent(_ ev: NostrEvent, pubkey: Pubkey)
|
||||
var filter: (NostrEvent) -> Bool { get }
|
||||
var favorites: Set<Pubkey> { get }
|
||||
}
|
||||
26
damus/Features/ContactCard/ContactCardManagerMock.swift
Normal file
26
damus/Features/ContactCard/ContactCardManagerMock.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
class ContactCardManagerMock: ContactCard {
|
||||
var event: NostrEvent?
|
||||
var favorites: Set<Pubkey> = []
|
||||
|
||||
func isFavorite(_ pubkey: Pubkey) -> Bool {
|
||||
favorites.contains(pubkey)
|
||||
}
|
||||
|
||||
func toggleFavorite(_ pubkey: Pubkey, postbox: PostBox, keyPair: FullKeypair?) {
|
||||
if favorites.contains(pubkey) {
|
||||
favorites.remove(pubkey)
|
||||
} else {
|
||||
favorites.insert(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEvent(_ ev: NostrEvent, pubkey: Pubkey) {
|
||||
event = ev
|
||||
}
|
||||
|
||||
var filter: ((_ ev: NostrEvent) -> Bool) {
|
||||
{ ev in self.favorites.contains(ev.pubkey) }
|
||||
}
|
||||
}
|
||||
40
damus/Features/ContactCard/Views/FavoriteButtonView.swift
Normal file
40
damus/Features/ContactCard/Views/FavoriteButtonView.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FavoriteButtonView: View {
|
||||
let pubkey: Pubkey
|
||||
let damus_state: DamusState
|
||||
|
||||
@State private var favorite: Bool
|
||||
|
||||
init(pubkey: Pubkey, damus_state: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
self.damus_state = damus_state
|
||||
self._favorite = State(initialValue: damus_state.contactCards.isFavorite(pubkey))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(
|
||||
action: {
|
||||
damus_state.contactCards.toggleFavorite(
|
||||
pubkey,
|
||||
postbox: damus_state.nostrNetwork.postbox,
|
||||
keyPair: damus_state.keypair.to_full()
|
||||
)
|
||||
favorite.toggle()
|
||||
}) {
|
||||
Image(favorite ? "heart.fill" : "heart")
|
||||
.foregroundColor(favorite ? DamusColors.purple : .primary)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct FavoriteButtonView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FavoriteButtonView(
|
||||
pubkey: test_pubkey,
|
||||
damus_state: test_damus_state
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
||||
case .zap, .zap_request:
|
||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||
return .loaded(route: Route.Zaps(target: zap.target))
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list:
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .contact_card:
|
||||
return .unknown_or_unsupported_kind
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
|
||||
@@ -96,6 +96,13 @@ class Contacts {
|
||||
func get_friended_followers(_ pubkey: Pubkey) -> [Pubkey] {
|
||||
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
||||
}
|
||||
|
||||
var friend_filter: (NostrEvent) -> Bool {
|
||||
{ [weak self] ev in
|
||||
guard let self else { return false }
|
||||
return self.is_friend(ev.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate protocol for `Contacts`. Use this to listen to significant updates from a `Contacts` instance
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ProfileActionSheetView: View {
|
||||
@StateObject var profile: ProfileModel
|
||||
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
@State private var favorite: Bool
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -25,6 +26,7 @@ struct ProfileActionSheetView: View {
|
||||
self.damus_state = damus_state
|
||||
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
|
||||
self.navigationHandler = navigationHandler
|
||||
self.favorite = damus_state.contactCards.isFavorite(pubkey)
|
||||
}
|
||||
|
||||
func imageBorderColor() -> Color {
|
||||
@@ -69,6 +71,30 @@ struct ProfileActionSheetView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var favoriteButton: some View {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
Button(
|
||||
action: {
|
||||
damus_state.contactCards.toggleFavorite(
|
||||
profile.pubkey,
|
||||
postbox: damus_state.nostrNetwork.postbox,
|
||||
keyPair: damus_state.keypair.to_full()
|
||||
)
|
||||
favorite = damus_state.contactCards.isFavorite(profile.pubkey)
|
||||
},
|
||||
label: {
|
||||
Image("heart.fill")
|
||||
.foregroundColor(favorite ? DamusColors.deepPurple : .primary)
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
)
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
Text("Favorite", comment: "Button label that allows the user to favorite the user shown on-screen")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
var dmButton: some View {
|
||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||
return VStack(alignment: .center, spacing: 10) {
|
||||
@@ -121,17 +147,18 @@ struct ProfileActionSheetView: View {
|
||||
AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center)
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 20) {
|
||||
self.followButton
|
||||
self.zapButton
|
||||
self.dmButton
|
||||
followButton
|
||||
favoriteButton
|
||||
zapButton
|
||||
dmButton
|
||||
if damus_state.keypair.pubkey != profile.pubkey && damus_state.keypair.privkey != nil {
|
||||
self.muteButton
|
||||
muteButton
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
self.navigate(route: Route.ProfileByKey(pubkey: profile.pubkey))
|
||||
|
||||
@@ -270,6 +270,7 @@ struct ProfileView: View {
|
||||
|
||||
func actionSection(record: ProfileRecord?, pubkey: Pubkey) -> some View {
|
||||
return Group {
|
||||
FavoriteButtonView(pubkey: profile.pubkey, damus_state: damus_state)
|
||||
if let record,
|
||||
let profile = record.profile,
|
||||
let lnurl = record.lnurl,
|
||||
|
||||
@@ -13,15 +13,16 @@ struct SignalView: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if signal.signal != signal.max_signal {
|
||||
NavigationLink(value: Route.RelayConfig) {
|
||||
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(width:50,height:30)
|
||||
.opacity(signal.signal != signal.max_signal ? 1 : 0)
|
||||
.disabled(signal.signal == signal.max_signal)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,20 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Timeline source determines whether to show content from follows or favorites
|
||||
enum TimelineSource: CustomStringConvertible {
|
||||
case follows
|
||||
case favorites
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .follows:
|
||||
return NSLocalizedString("Follows", comment: "Show Notes from your following")
|
||||
case .favorites:
|
||||
return NSLocalizedString("Favorites", comment: "Show Notes from your favorites")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||
enum FilterState : Int {
|
||||
|
||||
@@ -174,6 +174,15 @@ class HomeModel: ContactsDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Force refresh of home timeline filters, bypassing startup debounce
|
||||
/// Used when favorites are fetched from network during startup to ensure unfollowed favorited users are included
|
||||
/// This is needed because the normal resubscribe path is blocked during initial load.
|
||||
/// TODO: Will this be a performance problem?
|
||||
func refresh_home_filters() {
|
||||
unsubscribe_to_home_filters()
|
||||
subscribe_to_home_filters()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func process_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) {
|
||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||
@@ -201,6 +210,8 @@ class HomeModel: ContactsDelegate {
|
||||
handle_old_list_event(ev)
|
||||
case .mute_list:
|
||||
handle_mute_list_event(ev)
|
||||
case .contact_card:
|
||||
damus_state.contactCards.loadEvent(ev, pubkey: damus_state.pubkey)
|
||||
case .boost:
|
||||
handle_boost_event(sub_id: sub_id, ev)
|
||||
case .like:
|
||||
@@ -560,6 +571,9 @@ class HomeModel: ContactsDelegate {
|
||||
our_old_blocklist_filter.parameter = ["mute"]
|
||||
our_old_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var contact_cards_filter = NostrFilter(kinds: [.contact_card])
|
||||
contact_cards_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter(kinds: [.mute_list])
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
@@ -587,7 +601,7 @@ class HomeModel: ContactsDelegate {
|
||||
|
||||
var notifications_filters = [notifications_filter]
|
||||
let contacts_filter_chunks = contacts_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
|
||||
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
|
||||
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter, contact_cards_filter]
|
||||
var dms_filters = [dms_filter, our_dms_filter]
|
||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||
|
||||
@@ -649,6 +663,16 @@ class HomeModel: ContactsDelegate {
|
||||
home_filters.append(hashtag_filter)
|
||||
}
|
||||
|
||||
// Add filter for favorited users who we dont follow
|
||||
let all_favorites = damus_state.contactCards.favorites
|
||||
let favorited_not_followed = Array(all_favorites.subtracting(Set(friends)))
|
||||
if !favorited_not_followed.isEmpty {
|
||||
var favorites_filter = NostrFilter(kinds: home_filter_kinds)
|
||||
favorites_filter.authors = favorited_not_followed
|
||||
favorites_filter.limit = 500
|
||||
home_filters.append(favorites_filter)
|
||||
}
|
||||
|
||||
let relay_ids = relay_id.map { [$0] }
|
||||
home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
|
||||
let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
|
||||
|
||||
@@ -30,10 +30,10 @@ struct InnerTimelineView: View {
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
let events = self.events.events
|
||||
if events.isEmpty {
|
||||
let evs = events.filter(filter)
|
||||
if evs.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
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// PostingTimelineSwitcherView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Askia Linder on 2025-09-06.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TipKit
|
||||
|
||||
struct PostingTimelineSwitcherView: View {
|
||||
let damusState: DamusState
|
||||
@Binding var timelineSource: TimelineSource
|
||||
|
||||
init(damusState: DamusState, timelineSource: Binding<TimelineSource>) {
|
||||
self.damusState = damusState
|
||||
self._timelineSource = timelineSource
|
||||
if #available(iOS 17.0, *) {
|
||||
try? Tips.configure([.displayFrequency(.daily)])
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker(selection: $timelineSource) {
|
||||
Label(TimelineSource.follows.description, image: "user-added")
|
||||
.tag(TimelineSource.follows)
|
||||
Label(TimelineSource.favorites.description, image: "heart")
|
||||
.tag(TimelineSource.favorites)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.onAppear() {
|
||||
if #available(iOS 17.0, *) {
|
||||
TimelineSwitcherTip.shared.invalidate(reason: .actionPerformed)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "square.stack")
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.frame(height: 35)
|
||||
}
|
||||
.menuOrder(.fixed)
|
||||
.accessibilityLabel(NSLocalizedString("Timeline switcher, select \(TimelineSource.follows.description) or \(TimelineSource.favorites.description)", comment: "Accessibility label for the timeline switcher button at the topbar"))
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
struct TimelineSwitcherTip: Tip {
|
||||
static let shared = TimelineSwitcherTip()
|
||||
|
||||
var options: [Option] {
|
||||
[MaxDisplayCount(1)]
|
||||
}
|
||||
|
||||
var title: Text {
|
||||
Text("Timeline switcher", comment: "Title of tip that informs users that they can switch timelines.")
|
||||
}
|
||||
|
||||
var message: Text? {
|
||||
Text("Switch between posts from your follows or your favorites.", comment: "Description of the tip that informs users that they can switch between posts from your follows or your favorites.")
|
||||
}
|
||||
|
||||
var image: Image? {
|
||||
Image(systemName: "square.stack")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PostingTimelineSwitcherView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PostingTimelineSwitcherView(
|
||||
damusState: test_damus_state,
|
||||
timelineSource: .constant(.follows)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,17 @@ struct PostingTimelineView: View {
|
||||
@State var headerHeight: CGFloat = 0
|
||||
@Binding var headerOffset: CGFloat
|
||||
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
@State var timeline_source: TimelineSource = .follows
|
||||
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
filters.append(fstate.filter)
|
||||
switch timeline_source {
|
||||
case .follows:
|
||||
filters.append(damus_state.contacts.friend_filter)
|
||||
case .favorites:
|
||||
filters.append(damus_state.contactCards.filter)
|
||||
}
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
@@ -48,6 +55,22 @@ struct PostingTimelineView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
SignalView(state: damus_state, signal: home.signal)
|
||||
let switchView = PostingTimelineSwitcherView(
|
||||
damusState: damus_state,
|
||||
timelineSource: $timeline_source
|
||||
)
|
||||
if #available(iOS 17.0, *) {
|
||||
switchView
|
||||
.popoverTip(PostingTimelineSwitcherView.TimelineSwitcherTip.shared)
|
||||
} else {
|
||||
switchView
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.overlay {
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
@@ -57,16 +80,8 @@ struct PostingTimelineView: View {
|
||||
.onTapGesture {
|
||||
isSideBarOpened.toggle()
|
||||
}
|
||||
.padding(.leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
SignalView(state: damus_state, signal: home.signal)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
@@ -126,3 +141,15 @@ struct PostingTimelineView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PostingTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PostingTimelineView(
|
||||
damus_state: test_damus_state,
|
||||
home: HomeModel(),
|
||||
isSideBarOpened: .constant(false),
|
||||
active_sheet: .constant(nil),
|
||||
headerOffset: .constant(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
16
damus/Notify/ContactCardNotify.swift
Normal file
16
damus/Notify/ContactCardNotify.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
struct FavoriteNotify: Notify {
|
||||
typealias Payload = Void
|
||||
var payload: Void
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var favoriteUpdated: NotifyHandler<FavoriteNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func favoriteUpdated() -> Notifications<FavoriteNotify> {
|
||||
.init(.init(payload: ()))
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ var test_damus_state: DamusState = ({
|
||||
likes: .init(our_pubkey: our_pubkey),
|
||||
boosts: .init(our_pubkey: our_pubkey),
|
||||
contacts: .init(our_pubkey: our_pubkey),
|
||||
contactCards: ContactCardManagerMock(),
|
||||
mutelist_manager: MutelistManager(user_keypair: test_keypair),
|
||||
profiles: .init(ndb: ndb),
|
||||
dms: .init(our_pubkey: our_pubkey),
|
||||
|
||||
@@ -30,7 +30,9 @@ func generate_test_damus_state(
|
||||
let damus = DamusState(keypair: test_keypair,
|
||||
likes: .init(our_pubkey: our_pubkey),
|
||||
boosts: .init(our_pubkey: our_pubkey),
|
||||
contacts: .init(our_pubkey: our_pubkey), mutelist_manager: mutelist_manager,
|
||||
contacts: .init(our_pubkey: our_pubkey),
|
||||
contactCards: ContactCardManagerMock(),
|
||||
mutelist_manager: mutelist_manager,
|
||||
profiles: profiles,
|
||||
dms: .init(our_pubkey: our_pubkey),
|
||||
previews: .init(),
|
||||
|
||||
249
damusTests/Models/ContactCardManagerTests.swift
Normal file
249
damusTests/Models/ContactCardManagerTests.swift
Normal file
@@ -0,0 +1,249 @@
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
final class ContactCardManagerTests: XCTestCase {
|
||||
|
||||
func testInitialization() {
|
||||
// Given: The shared ContactCardManager instance
|
||||
let manager = ContactCardManager()
|
||||
|
||||
// Then: It should have an empty favorites set
|
||||
XCTAssertTrue(manager.favorites.isEmpty)
|
||||
}
|
||||
|
||||
func testIsFavorite_WhenEmpty_ReturnsFalse() {
|
||||
// Given: An empty favorites manager
|
||||
let sut = ContactCardManager()
|
||||
let pubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
|
||||
// When: Checking if a pubkey is favorite
|
||||
let result = sut.isFavorite(pubkey)
|
||||
|
||||
// Then: Should return false
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
func testIsFavorite_WhenPubkeyExists_ReturnsTrue() {
|
||||
// Given: A pubkey added to favorites
|
||||
let sut = ContactCardManager()
|
||||
let pubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
sut.toggleFavorite(pubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: generate_new_keypair())
|
||||
|
||||
// When: Checking if the pubkey is favorite
|
||||
let result = sut.isFavorite(pubkey)
|
||||
|
||||
// Then: Should return true
|
||||
XCTAssertTrue(result)
|
||||
}
|
||||
|
||||
func testIsFavorite_WhenPubkeyDoesNotExist_ReturnsFalse() {
|
||||
// Given: A different pubkey added to favorites
|
||||
let sut = ContactCardManager()
|
||||
let expected = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let differentPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
sut.toggleFavorite(expected, postbox: test_damus_state.nostrNetwork.postbox, keyPair: generate_new_keypair())
|
||||
|
||||
// When: Checking if a different pubkey is favorite
|
||||
let result = sut.isFavorite(differentPubkey)
|
||||
|
||||
// Then: Should return false
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
func testToggleFavorite_WhenNotFavorite_AddsToFavorites() {
|
||||
// Given: A pubkey not in favorites
|
||||
let sut = ContactCardManager()
|
||||
let pubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
XCTAssertFalse(sut.isFavorite(pubkey))
|
||||
|
||||
// When: Toggling the pubkey
|
||||
sut.toggleFavorite(pubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: generate_new_keypair())
|
||||
|
||||
// Then: Should be added to favorites
|
||||
XCTAssertTrue(sut.isFavorite(pubkey))
|
||||
XCTAssertEqual(sut.favorites.count, 1)
|
||||
}
|
||||
|
||||
func testToggleFavorite_WhenAlreadyFavorite_RemovesFromFavorites() {
|
||||
// Given: A pubkey already in favorites
|
||||
let sut = ContactCardManager()
|
||||
let pubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let keypair = generate_new_keypair()
|
||||
sut.toggleFavorite(pubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: keypair)
|
||||
XCTAssertTrue(sut.isFavorite(pubkey))
|
||||
|
||||
// When: Toggling the pubkey again
|
||||
sut.toggleFavorite(pubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: keypair)
|
||||
|
||||
// Then: Should be removed from favorites
|
||||
XCTAssertFalse(sut.isFavorite(pubkey))
|
||||
XCTAssertEqual(sut.favorites.count, 0)
|
||||
}
|
||||
|
||||
func testloadEvent_WithContactCard_AddsToFavorites() {
|
||||
// Given: A contact card event for favorites
|
||||
let sut = ContactCardManager()
|
||||
let targetPubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let userPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
|
||||
let tags = [
|
||||
[ContactCardManager.TARGET_PUBLIC_KEY, targetPubkey.hex()],
|
||||
[ContactCardManager.CONTACT_SET, ContactCardManager.FAVORITE_TAG]
|
||||
]
|
||||
|
||||
let event = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: tags
|
||||
)!
|
||||
|
||||
// When: Handling the contact card event
|
||||
sut.loadEvent(event, pubkey: userPubkey)
|
||||
|
||||
// Then: Should add the target pubkey to favorites
|
||||
XCTAssertTrue(sut.isFavorite(targetPubkey))
|
||||
}
|
||||
|
||||
func testloadEvent_WithContactCard_RemovesFromFavorites() {
|
||||
// Given: A contact card event without favorite tag (unfavorite)
|
||||
let sut = ContactCardManager()
|
||||
let targetPubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let userPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
|
||||
// First add to favorites
|
||||
sut.toggleFavorite(targetPubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: generate_new_keypair())
|
||||
XCTAssertTrue(sut.isFavorite(targetPubkey))
|
||||
|
||||
// Create unfavorite contact card (only target public key tag, no contact set tag)
|
||||
let tags = [
|
||||
[ContactCardManager.TARGET_PUBLIC_KEY, targetPubkey.hex()]
|
||||
]
|
||||
|
||||
let event = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: tags,
|
||||
createdAt: UInt32(Date().timeIntervalSince1970) + 1
|
||||
)!
|
||||
|
||||
// When: Handling the unfavorite contact card event
|
||||
sut.loadEvent(event, pubkey: userPubkey)
|
||||
|
||||
// Then: Should remove the target pubkey from favorites
|
||||
XCTAssertFalse(sut.isFavorite(targetPubkey))
|
||||
}
|
||||
|
||||
func testloadEvent_WithMissingTargetPubkey_ReturnsEarly() {
|
||||
// Given: A contact card event without d tag (missing target pubkey)
|
||||
let sut = ContactCardManager()
|
||||
let userPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
let initialFavoritesCount = sut.favorites.count
|
||||
let tags = [
|
||||
[ContactCardManager.CONTACT_SET, ContactCardManager.FAVORITE_TAG]
|
||||
]
|
||||
let event = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: tags
|
||||
)!
|
||||
|
||||
// When: Handling the event with missing target pubkey
|
||||
sut.loadEvent(event, pubkey: userPubkey)
|
||||
|
||||
// Then: Should return early without changing favorites
|
||||
XCTAssertEqual(sut.favorites.count, initialFavoritesCount)
|
||||
}
|
||||
|
||||
func testloadEvent_WithInvalidTargetPubkey_ReturnsEarly() {
|
||||
// Given: A contact card event with invalid d tag (invalid pubkey hex)
|
||||
let sut = ContactCardManager()
|
||||
let userPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
let initialFavoritesCount = sut.favorites.count
|
||||
// Create contact card with invalid pubkey hex
|
||||
let tags = [
|
||||
[ContactCardManager.TARGET_PUBLIC_KEY, "invalid_hex"],
|
||||
[ContactCardManager.CONTACT_SET, ContactCardManager.FAVORITE_TAG]
|
||||
]
|
||||
let event = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: tags
|
||||
)!
|
||||
|
||||
// When: Handling the event with invalid target pubkey
|
||||
sut.loadEvent(event, pubkey: userPubkey)
|
||||
|
||||
// Then: Should return early without changing favorites
|
||||
XCTAssertEqual(sut.favorites.count, initialFavoritesCount)
|
||||
}
|
||||
|
||||
func testloadEvent_WithOlderEvent_ReturnsEarly() {
|
||||
// Given: An existing newer contact card event
|
||||
let sut = ContactCardManager()
|
||||
let targetPubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let userPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
// Create newer favorite event first
|
||||
let newerTags = [
|
||||
[ContactCardManager.TARGET_PUBLIC_KEY, targetPubkey.hex()],
|
||||
[ContactCardManager.CONTACT_SET, ContactCardManager.FAVORITE_TAG]
|
||||
]
|
||||
let newerEvent = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: newerTags,
|
||||
createdAt: 1000
|
||||
)!
|
||||
sut.loadEvent(newerEvent, pubkey: userPubkey)
|
||||
XCTAssertTrue(sut.isFavorite(targetPubkey))
|
||||
// Create older unfavorite event
|
||||
let olderTags = [
|
||||
[ContactCardManager.TARGET_PUBLIC_KEY, targetPubkey.hex()]
|
||||
]
|
||||
let olderEvent = NostrEvent(
|
||||
content: "",
|
||||
keypair: Keypair(pubkey: userPubkey, privkey: nil),
|
||||
kind: NostrKind.contact_card.rawValue,
|
||||
tags: olderTags,
|
||||
createdAt: 500 // Older timestamp
|
||||
)!
|
||||
|
||||
// When: Handling the older event
|
||||
sut.loadEvent(olderEvent, pubkey: userPubkey)
|
||||
|
||||
// Then: Should ignore the older event and keep the favorite status
|
||||
XCTAssertTrue(sut.isFavorite(targetPubkey))
|
||||
}
|
||||
|
||||
func testFilter_WithFavoritePubkey_ReturnsTrue() {
|
||||
// Given: A pubkey in favorites
|
||||
let sut = ContactCardManager()
|
||||
let favoritePubkey = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
|
||||
let otherPubkey = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
|
||||
sut.toggleFavorite(favoritePubkey, postbox: test_damus_state.nostrNetwork.postbox, keyPair: generate_new_keypair())
|
||||
// Create events from both pubkeys
|
||||
let favoriteEvent = NostrEvent(
|
||||
content: "Hello from favorite",
|
||||
keypair: Keypair(pubkey: favoritePubkey, privkey: nil),
|
||||
kind: NostrKind.text.rawValue,
|
||||
tags: []
|
||||
)!
|
||||
let otherEvent = NostrEvent(
|
||||
content: "Hello from other",
|
||||
keypair: Keypair(pubkey: otherPubkey, privkey: nil),
|
||||
kind: NostrKind.text.rawValue,
|
||||
tags: []
|
||||
)!
|
||||
|
||||
// When: Using the filter
|
||||
let filter = sut.filter
|
||||
|
||||
// Then: Should return true for favorite, false for other
|
||||
XCTAssertTrue(filter(favoriteEvent))
|
||||
XCTAssertFalse(filter(otherEvent))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user