Compare commits

...

14 Commits

Author SHA1 Message Date
William Casarin b31b917b70 Merge remote-tracking branch 'github/quote-reposts'
This adds quote repost listing support to Damus!

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 09:33:29 +00:00
William Casarin c521998158 ui: add quoted reposts view to threads
This adds quote reposts as an additional detail view on threads. It will
list quoted reposts that have the `q` tag. Not all clients have updated
to this yet (like primal), but hopefully they will soon.

Changelog-Added: Show list of quoted reposts in threads
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 3f1f257df2 model: upgrade EventsModel to support quote reposts queries
We also switch to using an eventholder because that is a bit nicer
when it comes to rendering quotes in timelines.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 1339ec3ded note: add is_quote_repost helper
This will be used for excluding quote repost notes from threads

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 770a845b36 filters: add ContentFilters helper constructor
This is slightly faster for timeline code that needs default filters

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 68dd47130e eventsmodel: remove inheritence in Reactions/Reposts model
Simplify with new EventsModel constructors. This is slightly less
typesafe but its not a big deal, I hate inheritence more.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 8cdbc84093 home: add quote repost counter and handler
This adds the initial support code for counting and handling
quote reposts.

Eventually we are going to replace all of the event counts by stats
within nostrdb, but we do this in the meantime now.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-17 08:54:35 +00:00
William Casarin 6111e244de filter: add reposts query filter helper
Add a filter helper to easily query quote repost queries.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-16 12:03:08 +00:00
William Casarin 0043f0059d strings: add pluralized quoted_repost_count string
We will be using this for translating "Quote{,s}" pluralization. For now
we add the english version.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-16 11:59:56 +00:00
alltheseas 4413ec0ec5
Update README.md
added reference to nip-04 encrypted DM support, including link to nostr-protocol nip-04 page
2024-03-13 10:06:01 -05:00
ericholguin 9a83872a22 ui: Add proxy view to selected events
This patch adds the proxy view to selected events.

Fixes: https://github.com/damus-io/damus/issues/2033
Changelog-Added: Proxy Tags are now viewable on Selected Events
Signed-off-by: ericholguin <ericholguin@apache.org>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-12 09:35:28 +00:00
ericholguin 988da17b06 Minor Fixes
This adds the recommended parameter to the relay view used in Wallet view.

Signed-off-by: ericholguin <ericholguin@apache.org>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-12 09:34:21 +00:00
ericholguin 5e530bfc9c ui: Wallet View redesign + Mutiny Wallet integration
This patch redesigns the wallet view to more closely match Rob's design.
In addition this patch allows users to connect to Mutiny Wallet by clicking a button.

iPhone SE (3rd generation) Dark Mode:
https://v.nostr.build/K9lk.mp4

iPhone 15 Pro Max Light Mode:
https://v.nostr.build/9mKA.mp4

Connected Alby Wallet:
https://i.nostr.build/kyd5.png

Changelog-Added: Connect to Mutiny Wallet Button
Changelog-Changed: Moved paste nwc button to main wallet view
Changelog-Changed: Errors with an NWC will show as an alert
Changelog-Fixed: Issue where NWC Scanner view would not dismiss after a failed scan/paste
Signed-off-by: ericholguin <ericholguin@apache.org>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240310223713.4541-1-ericholguin@apache.org
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-11 10:12:05 +00:00
ericholguin 0719e94fbc ux: Relay View Improvements
This patch removes the Recommended Relay View and the old representation of recommended relays.
Adds a tab view style to the Relay Config View allowing the user to switch between their connected relays
and recommended relays. They can add and remove from the recommended view as well. For users logged in with
a pubkey the add button will no longer be displayed.

Testing
——
iPhone 15 Pro Max (17.0) Light Mode:
https://v.nostr.build/QGMZ.mp4

iPhone SE (3rd generation) (16.4) Dark Mode:
https://v.nostr.build/Wlw3.mp4
——

Changelog-Changed: Relay config view user interface

Signed-off-by: ericholguin <ericholguin@apache.org>
Link: 20240307152808.47929-1-ericholguin@apache.org
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-11 10:03:49 +00:00
40 changed files with 768 additions and 562 deletions

View File

@ -30,6 +30,7 @@ Damus has also graciously received donations or grants from hundreds of Damus us
damus implements the following [Nostr Implementation Possibilities][nips]
- [NIP-01: Basic protocol flow][nip01]
- [NIP-04: Encrypted direct message][nip04]
- [NIP-08: Mentions][nip08]
- [NIP-10: Reply conventions][nip10]
- [NIP-12: Generic tag queries (hashtags)][nip12]
@ -41,6 +42,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
[nips]: https://github.com/nostr-protocol/nips
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
[nip04]: https://github.com/nostr-protocol/nips/blob/master/04.md
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md

View File

@ -25,7 +25,6 @@
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; };
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
@ -61,7 +60,6 @@
4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253652A76D0FF0004F4B8 /* OnlyZapsNotify.swift */; };
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253672A76D2470004F4B8 /* MuteNotify.swift */; };
4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */; };
4C12536C2A76D4B00004F4B8 /* RepostedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */; };
4C15C7152A55DE7A00D0A0DB /* ReactionsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */; };
4C190F202A535FC200027FD5 /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */; };
4C190F252A547D2000027FD5 /* LoadScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F242A547D2000027FD5 /* LoadScript.swift */; };
@ -255,6 +253,7 @@
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9276E2A2A5D110098A105 /* wasm.c */; };
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4F14A92A2A71AB0045A0B9 /* nostrscript.c */; };
4C9147002A2A891E00DDEA40 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C9146FF2A2A891E00DDEA40 /* error.c */; };
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; };
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */; };
@ -288,14 +287,12 @@
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */; };
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838529656C8B00DC99E7 /* NIP05.swift */; };
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */; };
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */; };
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838C296F710400DC99E7 /* Reposted.swift */; };
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; };
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
@ -404,6 +401,8 @@
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; };
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; };
5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
@ -769,7 +768,6 @@
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
@ -843,7 +841,6 @@
4C1253652A76D0FF0004F4B8 /* OnlyZapsNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlyZapsNotify.swift; sourceTree = "<group>"; };
4C1253672A76D2470004F4B8 /* MuteNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteNotify.swift; sourceTree = "<group>"; };
4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaysChangedNotify.swift; sourceTree = "<group>"; };
4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedNotify.swift; sourceTree = "<group>"; };
4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsSettingsView.swift; sourceTree = "<group>"; };
4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; };
4C190F242A547D2000027FD5 /* LoadScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadScript.swift; sourceTree = "<group>"; };
@ -1165,6 +1162,7 @@
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C9146FF2A2A891E00DDEA40 /* error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = error.c; sourceTree = "<group>"; };
4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteRepostsView.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; };
4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttrStringTestExtensions.swift; sourceTree = "<group>"; };
@ -1205,14 +1203,12 @@
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelaysView.swift; sourceTree = "<group>"; };
4CB8838529656C8B00DC99E7 /* NIP05.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05.swift; sourceTree = "<group>"; };
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBar.swift; sourceTree = "<group>"; };
4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05Badge.swift; sourceTree = "<group>"; };
4CB8838C296F710400DC99E7 /* Reposted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reposted.swift; sourceTree = "<group>"; };
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; };
4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; };
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
@ -1326,6 +1322,8 @@
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; };
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; };
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; };
5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; };
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; };
@ -1510,6 +1508,7 @@
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */,
);
path = Reposts;
sourceTree = "<group>";
@ -1588,7 +1587,6 @@
BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
@ -1618,7 +1616,6 @@
4C216F372871EDE300040376 /* DirectMessageModel.swift */,
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
@ -2112,6 +2109,7 @@
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
4C687C202A5F7ED00092C550 /* DamusBackground.swift */,
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */,
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */,
);
path = Gradients;
sourceTree = "<group>";
@ -2182,6 +2180,7 @@
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
F71694F32A6732B7001F4053 /* GradientFollowButton.swift */,
4C7D09652A0AE62100943473 /* AlbyButton.swift */,
5C7389B62B9E692E00781E0A /* MutinyButton.swift */,
);
path = Buttons;
sourceTree = "<group>";
@ -2258,7 +2257,6 @@
4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */,
4C1253672A76D2470004F4B8 /* MuteNotify.swift */,
4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */,
4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */,
4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */,
4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */,
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */,
@ -2294,7 +2292,6 @@
isa = PBXGroup;
children = (
4CE879532996BA0000F758CC /* Detail */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
@ -2741,14 +2738,6 @@
path = DamusNotificationService;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
E06336A82B7582E000A88E6B /* img_with_location.jpeg */,
);
path = Assets;
sourceTree = "<group>";
};
D7CBD1D22B8D21C100BFD889 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -2757,6 +2746,14 @@
path = Extensions;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
E06336A82B7582E000A88E6B /* img_with_location.jpeg */,
);
path = Assets;
sourceTree = "<group>";
};
F71694E82A66221E001F4053 /* Onboarding */ = {
isa = PBXGroup;
children = (
@ -3052,6 +3049,7 @@
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
4CDD1AE22A6B3074001CD4DF /* NdbTagsIterator.swift in Sources */,
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */,
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */,
D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */,
@ -3109,7 +3107,6 @@
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */,
4C687C272A6039500092C550 /* TestData.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */,
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
@ -3254,6 +3251,7 @@
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */,
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */,
4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
@ -3325,7 +3323,6 @@
4C32B95E2A9AD44700DC3548 /* FlatBufferObject.swift in Sources */,
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
50A16FFD2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift in Sources */,
@ -3344,6 +3341,7 @@
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */,
4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */,
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
@ -3416,8 +3414,6 @@
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
7527271E2A93FF0100214108 /* Block.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4C12536C2A76D4B00004F4B8 /* RepostedNotify.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4C32B9592A9AD44700DC3548 /* Table.swift in Sources */,
4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */,

View File

@ -0,0 +1,20 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "mutiny.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
//
// MutinyGradient.swift
// damus
//
// Created by eric on 3/9/24.
//
import SwiftUI
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
let MutinyGradient: LinearGradient =
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)

View File

@ -723,7 +723,8 @@ struct ContentView: View {
nav: self.navigationCoordinator,
music: MusicController(onChange: music_changed),
video: VideoController(),
ndb: ndb
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey)
)
home.damus_state = self.damus_state!

View File

@ -16,10 +16,12 @@ enum Zapped {
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_quote_repost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zapping?
@Published var likes: Int
@Published var boosts: Int
@Published var quote_reposts: Int
@Published private(set) var zaps: Int
@Published var zap_total: Int64
@Published var replies: Int
@ -28,7 +30,7 @@ class ActionBarModel: ObservableObject {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
}
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) {
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
@ -38,6 +40,8 @@ class ActionBarModel: ObservableObject {
self.our_boost = our_boost
self.our_zap = our_zap
self.our_reply = our_reply
self.our_quote_repost = our_quote_repost
self.quote_reposts = quote_reposts
}
func update(damus: DamusState, evid: NoteId) {
@ -45,11 +49,13 @@ class ActionBarModel: ObservableObject {
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0
self.replies = damus.replies.get_replies(evid)
self.quote_reposts = damus.quote_reposts.counts[evid] ?? 0
self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid)
self.our_quote_repost = damus.quote_reposts.our_events[evid]
self.objectWillChange.send()
}
@ -68,4 +74,8 @@ class ActionBarModel: ObservableObject {
var boosted: Bool {
return our_boost != nil
}
var quoted: Bool {
return our_quote_repost != nil
}
}

View File

@ -53,6 +53,10 @@ struct ContentFilters {
}
extension ContentFilters {
static func default_filters(damus_state: DamusState) -> ContentFilters {
return ContentFilters(filters: ContentFilters.defaults(damus_state: damus_state))
}
static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] {
var filters = Array<(NostrEvent) -> Bool>()
if damus_state.settings.hide_nsfw_tagged_content {

View File

@ -13,6 +13,7 @@ class DamusState: HeadlessDamusState {
let keypair: Keypair
let likes: EventCounter
let boosts: EventCounter
let quote_reposts: EventCounter
let contacts: Contacts
let mutelist_manager: MutelistManager
let profiles: Profiles
@ -36,7 +37,7 @@ class DamusState: HeadlessDamusState {
let ndb: Ndb
var purple: DamusPurple
init(pool: RelayPool, 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, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
init(pool: RelayPool, 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, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) {
self.pool = pool
self.keypair = keypair
self.likes = likes
@ -66,6 +67,7 @@ class DamusState: HeadlessDamusState {
settings: settings,
keypair: keypair
)
self.quote_reposts = quote_reposts
}
@discardableResult
@ -129,7 +131,8 @@ class DamusState: HeadlessDamusState {
nav: NavigationCoordinator(),
music: nil,
video: VideoController(),
ndb: .empty
ndb: .empty,
quote_reposts: .init(our_pubkey: empty_pub)
)
}
}

View File

@ -7,25 +7,62 @@
import Foundation
class EventsModel: ObservableObject {
let state: DamusState
let target: NoteId
let kind: NostrKind
let kind: QueryKind
let sub_id = UUID().uuidString
let profiles_id = UUID().uuidString
@Published var events: [NostrEvent] = []
var events: EventHolder
@Published var loading: Bool
enum QueryKind {
case kind(NostrKind)
case quotes
}
init(state: DamusState, target: NoteId, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
self.kind = .kind(kind)
self.loading = true
self.events = EventHolder(on_queue: { ev in
preload_events(state: state, events: [ev])
})
}
init(state: DamusState, target: NoteId, query: EventsModel.QueryKind) {
self.state = state
self.target = target
self.kind = query
self.loading = true
self.events = EventHolder(on_queue: { ev in
preload_events(state: state, events: [ev])
})
}
public static func quotes(state: DamusState, target: NoteId) -> EventsModel {
EventsModel(state: state, target: target, query: .quotes)
}
public static func reposts(state: DamusState, target: NoteId) -> EventsModel {
EventsModel(state: state, target: target, kind: .boost)
}
public static func likes(state: DamusState, target: NoteId) -> EventsModel {
EventsModel(state: state, target: target, kind: .like)
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter(kinds: [kind])
filter.referenced_ids = [target]
var filter: NostrFilter
switch kind {
case .kind(let k):
filter = NostrFilter(kinds: [k])
filter.referenced_ids = [target]
case .quotes:
filter = NostrFilter(kinds: [.text])
filter.quotes = [target]
}
filter.limit = 500
return filter
}
@ -41,21 +78,17 @@ class EventsModel: ObservableObject {
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue,
ev.referenced_ids.last == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
if events.insert(ev) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
guard case .nostr_event(let nev) = ev, nev.subid == self.sub_id
else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
@ -66,10 +99,11 @@ class EventsModel: ObservableObject {
case .auth:
break
case .eose:
self.loading = false
guard let txn = NdbTxn(ndb: self.state.ndb) else {
return
}
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events.all_events), damus_state: state, txn: txn)
}
}
}

View File

@ -347,12 +347,19 @@ class HomeModel {
case .already_counted:
break
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.reposted(boosted))
notify(.update_stats(note_id: e))
}
}
func handle_quote_repost_event(_ ev: NostrEvent, target: NoteId) {
switch damus_state.quote_reposts.add_event(ev, target: target) {
case .already_counted:
break
case .success(let n):
notify(.update_stats(note_id: target))
}
}
func handle_like_event(_ ev: NostrEvent) {
guard let e = ev.last_refid() else {
// no id ref? invalid like event
@ -672,6 +679,10 @@ class HomeModel {
damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair)
damus_state.events.insert(ev)
if let quoted_event = ev.referenced_quote_ids.first {
handle_quote_repost_event(ev, target: quoted_event.note_id)
}
if sub_id == home_subid {
insert_home_event(ev)
} else if sub_id == notifications_subid {

View File

@ -1,16 +0,0 @@
//
// LikesModel.swift
// damus
//
// Created by William Casarin on 2023-01-11.
//
import Foundation
final class ReactionsModel: EventsModel {
init(state: DamusState, target: NoteId) {
super.init(state: state, target: target, kind: .like)
}
}

View File

@ -1,15 +0,0 @@
//
// RepostsModel.swift
// damus
//
// Created by Terry Yiu on 1/22/23.
//
import Foundation
final class RepostsModel: EventsModel {
init(state: DamusState, target: NoteId) {
super.init(state: state, target: target, kind: .boost)
}
}

View File

@ -56,6 +56,7 @@ class ThreadModel: ObservableObject {
func subscribe() {
var meta_events = NostrFilter()
var quote_events = NostrFilter()
var event_filter = NostrFilter()
var ref_events = NostrFilter()
@ -74,11 +75,14 @@ class ThreadModel: ObservableObject {
kinds.append(.like)
}
meta_events.kinds = kinds
meta_events.limit = 1000
quote_events.kinds = [.text]
quote_events.quotes = [event.id]
quote_events.limit = 1000
let base_filters = [event_filter, ref_events]
let meta_filters = [meta_events]
let meta_filters = [meta_events, quote_events]
print("subscribing to thread \(event.id) with sub_id \(base_subid)")
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
@ -90,7 +94,7 @@ class ThreadModel: ObservableObject {
return
}
let the_ev = damus_state.events.upsert(ev)
damus_state.events.upsert(ev)
damus_state.replies.count_replies(ev, keypair: keypair)
damus_state.events.add_replies(ev: ev, keypair: keypair)
@ -111,7 +115,13 @@ class ThreadModel: ObservableObject {
}
} else if ev.is_textlike {
self.add_event(ev, keypair: damus_state.keypair)
// handle thread quote reposts, we just count them instead of
// adding them to the thread
if let target = ev.is_quote_repost, target == self.event.id {
//let _ = self.damus_state.quote_reposts.add_event(ev, target: target)
} else {
self.add_event(ev, keypair: damus_state.keypair)
}
}
}

View File

@ -41,7 +41,7 @@ struct QuoteId: IdType, TagKey, TagConvertible {
self.id = data
}
/// Refer to this QuoteId as a NoteId
/// The note id being quoted
var note_id: NoteId {
NoteId(self.id)
}

View File

@ -18,6 +18,7 @@ struct NostrFilter: Codable, Equatable {
var authors: [Pubkey]?
var hashtag: [String]?
var parameter: [String]?
var quotes: [NoteId]?
private enum CodingKeys : String, CodingKey {
case ids
@ -26,13 +27,14 @@ struct NostrFilter: Codable, Equatable {
case pubkeys = "#p"
case hashtag = "#t"
case parameter = "#d"
case quotes = "#q"
case since
case until
case authors
case limit
}
init(ids: [NoteId]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [NoteId]? = nil, pubkeys: [Pubkey]? = nil, since: UInt32? = nil, until: UInt32? = nil, limit: UInt32? = nil, authors: [Pubkey]? = nil, hashtag: [String]? = nil) {
init(ids: [NoteId]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [NoteId]? = nil, pubkeys: [Pubkey]? = nil, since: UInt32? = nil, until: UInt32? = nil, limit: UInt32? = nil, authors: [Pubkey]? = nil, hashtag: [String]? = nil, quotes: [NoteId]? = nil) {
self.ids = ids
self.kinds = kinds
self.referenced_ids = referenced_ids
@ -42,6 +44,7 @@ struct NostrFilter: Codable, Equatable {
self.limit = limit
self.authors = authors
self.hashtag = hashtag
self.quotes = quotes
}
public static func copy(from: NostrFilter) -> NostrFilter {

View File

@ -1,26 +0,0 @@
//
// BoostedNotify.swift
// damus
//
// Created by William Casarin on 2023-07-30.
//
import Foundation
struct RepostedNotify: Notify {
typealias Payload = Counted
var payload: Payload
}
extension NotifyHandler {
static var reposted: NotifyHandler<RepostedNotify> {
.init()
}
}
extension Notifications {
static func reposted(_ counts: Counted) -> Notifications<RepostedNotify> {
.init(.init(payload: counts))
}
}

View File

@ -92,7 +92,9 @@ var test_damus_state: DamusState = ({
nav: .init(),
music: .init(onChange: {_ in }),
video: .init(),
ndb: ndb)
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey)
)
/*
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)

View File

@ -31,8 +31,9 @@ enum Route: Hashable {
case SearchSettings(settings: UserSettingsStore)
case DeveloperSettings(settings: UserSettingsStore)
case Thread(thread: ThreadModel)
case Reposts(reposts: RepostsModel)
case Reactions(reactions: ReactionsModel)
case Reposts(reposts: EventsModel)
case QuoteReposts(quotes: EventsModel)
case Reactions(reactions: EventsModel)
case Zaps(target: ZapTarget)
case Search(search: SearchModel)
case EULA
@ -53,7 +54,7 @@ enum Route: Hashable {
case .Followers(let followers):
FollowersView(damus_state: damusState, followers: followers)
case .Relay(let relay, let showActionButtons):
RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons)
RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons, recommended: false)
case .RelayDetail(let relay, let metadata):
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
case .Following(let following):
@ -92,6 +93,8 @@ enum Route: Hashable {
ThreadView(state: damusState, thread: thread)
case .Reposts(let reposts):
RepostsView(damus_state: damusState, model: reposts)
case .QuoteReposts(let quote_reposts):
QuoteRepostsView(damus_state: damusState, model: quote_reposts)
case .Reactions(let reactions):
ReactionsView(damus_state: damusState, model: reactions)
case .Zaps(let target):
@ -178,6 +181,9 @@ enum Route: Hashable {
case .Reposts(let reposts):
hasher.combine("reposts")
hasher.combine(reposts.target)
case .QuoteReposts(let evs_model):
hasher.combine("quote_reposts")
hasher.combine(evs_model.events.events.count)
case .Zaps(let target):
hasher.combine("zaps")
hasher.combine(target.id)

View File

@ -25,7 +25,7 @@ struct EventDetailBar: View {
var body: some View {
HStack {
if bar.boosts > 0 {
NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) {
NavigationLink(value: Route.Reposts(reposts: .reposts(state: state, target: target))) {
let nounString = pluralizedString(key: "reposts_count", count: bar.boosts)
let noun = Text(nounString).foregroundColor(.gray)
Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
@ -33,8 +33,17 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle())
}
if bar.quote_reposts > 0 {
NavigationLink(value: Route.QuoteReposts(quotes: .quotes(state: state, target: target))) {
let nounString = pluralizedString(key: "quoted_reposts_count", count: bar.quote_reposts)
let noun = Text(nounString).foregroundColor(.gray)
Text("\(Text(verbatim: bar.quote_reposts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many quoted reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 && !state.settings.onlyzaps_mode {
NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) {
NavigationLink(value: Route.Reactions(reactions: .likes(state: state, target: target))) {
let nounString = pluralizedString(key: "reactions_count", count: bar.likes)
let noun = Text(nounString).foregroundColor(.gray)
Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")

View File

@ -23,16 +23,15 @@ struct AlbyButton: View {
HStack {
Image("alby")
Text("Attach Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.")
Text("Connect to Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.")
.padding()
}
.offset(x: -25)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
.foregroundColor(DamusColors.black)
.background {
RoundedRectangle(cornerRadius: 24)
.fill(AlbyGradient, strokeBorder: colorScheme == .light ? DamusColors.black : DamusColors.white, lineWidth: 2)
RoundedRectangle(cornerRadius: 12)
.fill(AlbyGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white, lineWidth: 1)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
}
}
}

View File

@ -0,0 +1,47 @@
//
// MutinyButton.swift
// damus
//
// Created by eric on 3/9/24.
//
import SwiftUI
struct MutinyButton: View {
let action: () -> ()
@Environment(\.colorScheme) var colorScheme
init(action: @escaping () -> ()) {
self.action = action
}
var body: some View {
Button(action: {
action()
}) {
HStack {
Image("mutiny")
.resizable()
.frame(width: 45, height: 45)
Text("Connect to Mutiny Wallet", comment: "Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.")
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
.foregroundColor(DamusColors.white)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(MutinyGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white.opacity(0.2), lineWidth: 1)
}
}
}
}
struct MutinyButton_Previews: PreviewProvider {
static var previews: some View {
MutinyButton(action: {
print("mutiny button")
})
}
}

View File

@ -52,7 +52,11 @@ struct SelectedEventView: View {
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
.padding(.horizontal)
}
ProxyView(event: event)
.padding(.top, 5)
.padding(.horizontal)
EventBody(damus_state: damus, event: event, size: size, options: [.wide])
Mention

View File

@ -9,14 +9,14 @@ import SwiftUI
struct ReactionsView: View {
let damus_state: DamusState
@StateObject var model: ReactionsModel
@StateObject var model: EventsModel
@Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.events, id: \.id) { ev in
ForEach(model.events.events, id: \.id) { ev in
ReactionView(damus_state: damus_state, reaction: ev)
}
}
@ -38,6 +38,6 @@ struct ReactionsView: View {
struct ReactionsView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state
ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: test_note.id))
ReactionsView(damus_state: state, model: .likes(state: state, target: test_note.id))
}
}

View File

@ -1,131 +0,0 @@
//
// RecommendedRelayView.swift
// damus
//
// Created by William Casarin on 2022-12-29.
//
import SwiftUI
struct RecommendedRelayView: View {
let damus: DamusState
let relay: String
let add_button: Bool
let user_recommended: Bool
@ObservedObject private var model_cache: RelayModelCache
init(damus: DamusState, relay: String, add_button: Bool = true, user_recommended: Bool = false) {
self.damus = damus
self.relay = relay
self.add_button = add_button
self.user_recommended = user_recommended
self.model_cache = damus.relay_model_cache
}
var body: some View {
let meta = model_cache.model(with_relay_id: relay)?.metadata
if user_recommended {
HStack {
RelayPicView(relay: relay, icon: meta?.icon, size: 50, highlight: .none, disable_animation: false)
.padding(.horizontal, 5)
VStack(alignment: .leading) {
HStack {
Text(meta?.name ?? relay)
.font(.headline)
.padding(.bottom, 2)
RelayType(is_paid: damus.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
}
Text(relay)
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
if let keypair = damus.keypair.to_full() {
VStack(alignment: .center) {
if damus.pool.get_relay(relay) == nil {
AddButton(keypair: keypair)
} else {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(DamusColors.success)
.padding(.trailing, 10)
}
}
.padding(.horizontal, 5)
}
}
} else {
VStack {
RelayPicView(relay: relay, icon: meta?.icon, size: 70, highlight: .none, disable_animation: false)
if let meta = damus.relay_model_cache.model(with_relay_id: relay)?.metadata {
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta)){
EmptyView()
}
.opacity(0.0)
}
HStack {
Text(meta?.name ?? relay)
.lineLimit(1)
.frame(maxWidth: 150)
.padding(.vertical, 5)
}
.contextMenu {
CopyAction(relay: relay)
}
if let keypair = damus.keypair.to_full() {
AddButton(keypair: keypair)
}
}
}
}
func CopyAction(relay: String) -> some View {
Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
} label: {
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), image: "copy")
}
}
func AddButton(keypair: FullKeypair) -> some View {
Button(action: {
add_action(keypair: keypair)
}) {
Text(NSLocalizedString("Add", comment: "Button to add relay server to list."))
.padding(10)
}
.buttonStyle(NeutralButtonStyle())
}
func add_action(keypair: FullKeypair) {
guard let ev_before_add = damus.contacts.event else {
return
}
guard let relay_url = RelayURL(relay),
let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: damus.pool.our_descriptors, relay: relay_url, info: .rw) else {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: damus.pool.our_descriptors, keypair: keypair) {
damus.postbox.send(relay_metadata)
}
}
}
struct RecommendedRelayView_Previews: PreviewProvider {
static var previews: some View {
RecommendedRelayView(damus: test_damus_state, relay: "wss://relay.damus.io", user_recommended: true)
}
}

View File

@ -7,156 +7,83 @@
import SwiftUI
enum RelayTab: Int, CaseIterable{
case myRelays = 0
case recommended
var title: String{
switch self {
case .myRelays:
return "My relays"
case .recommended:
return "Recommended"
}
}
}
struct RelayConfigView: View {
let state: DamusState
@State var relays: [RelayDescriptor]
@State private var showActionButtons = false
@State var show_add_relay: Bool = false
@SceneStorage("RelayConfigView.show_recommended") var show_recommended : Bool = true
@State var selectedTab = 0
@Environment(\.dismiss) var dismiss
init(state: DamusState) {
self.state = state
_relays = State(initialValue: state.pool.our_descriptors)
UITabBar.appearance().isHidden = true
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
let recommended_relay_addresses = get_default_bootstrap_relays()
return recommended_relay_addresses.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = RelayURL(x) {
if let url = RelayURL(x) {
xs.append(RelayDescriptor(url: url, info: .rw))
}
}
}
var body: some View {
MainContent
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.our_descriptors
}
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
var MainContent: some View {
VStack {
Divider()
if showActionButtons && !show_recommended {
VStack {
Button(action: {
withAnimation(.easeOut(duration: 0.2)) {
show_recommended.toggle()
}
}) {
Text("Show recommended relays", comment: "Button to show recommended relays.")
.foregroundStyle(DamusLightGradient.gradient)
.padding(10)
.background {
RoundedRectangle(cornerRadius: 15)
.stroke(DamusLightGradient.gradient)
}
}
.padding(.top, 10)
}
}
if recommended.count > 0 && show_recommended {
VStack {
HStack(alignment: .top) {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 0.2)) {
show_recommended.toggle()
}
}) {
Image(systemName: "xmark.circle")
.font(.system(size: 18))
.foregroundStyle(DamusLightGradient.gradient)
}
.padding([.top, .trailing], 8)
}
Text("Recommended relays", comment: "Title for view of recommended relays.")
.foregroundStyle(DamusLightGradient.gradient)
.padding(10)
.background {
RoundedRectangle(cornerRadius: 15)
.stroke(DamusLightGradient.gradient)
}
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.id)
}
}
.padding(.horizontal, 30)
.padding(.vertical, 5)
}
.scrollIndicators(.hidden)
.mask(
HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [Color.clear, Color.white]), startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
Rectangle()
.fill(Color.white)
.frame(maxWidth: .infinity)
LinearGradient(gradient: Gradient(colors: [Color.white, Color.clear]), startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
}
)
.padding()
}
.frame(minWidth: 250, maxWidth: .infinity, minHeight: 250, alignment: .center)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(DamusLightGradient.gradient.opacity(0.15), strokeBorder: DamusLightGradient.gradient, lineWidth: 1)
}
.padding(.horizontal)
}
HStack {
Text(NSLocalizedString("My Relays", comment: "Section title for relay servers that the user is connected to."))
.font(.system(size: 32, weight: .bold))
NavigationView {
ZStack(alignment: .bottom){
TabView(selection: $selectedTab) {
RelayList(title: "My Relays", relayList: relays, recommended: false)
.tag(0)
Spacer()
Button(action: {
show_add_relay.toggle()
}) {
HStack {
Text(verbatim: "Add relay")
.padding(10)
RelayList(title: "Recommended", relayList: recommended, recommended: true)
.tag(1)
}
ZStack{
HStack{
ForEach((RelayTab.allCases), id: \.self){ item in
Button{
selectedTab = item.rawValue
} label: {
CustomTabItem(title: item.title, isActive: (selectedTab == item.rawValue))
}
}
}
}
.buttonStyle(NeutralButtonStyle())
.frame(width: 235, height: 35)
.background(.damusNeutral3)
.cornerRadius(30)
.padding(.horizontal, 26)
}
.padding(25)
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.id, showActionButtons: $showActionButtons)
}
.listStyle(PlainListStyle())
}
.navigationTitle(NSLocalizedString("Relays", comment: "Title of relays view"))
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.sheet(isPresented: $show_add_relay, onDismiss: { self.show_add_relay = false }) {
if #available(iOS 16.0, *) {
AddRelayView(state: state)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
AddRelayView(state: state)
}
AddRelayView(state: state)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
}
.toolbar {
if state.keypair.privkey != nil {
if state.keypair.privkey != nil && selectedTab == 0 {
if showActionButtons {
Button("Done") {
withAnimation {
@ -172,6 +99,65 @@ struct RelayConfigView: View {
}
}
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.our_descriptors
}
.onAppear {
notify(.display_tabbar(false))
}
.onDisappear {
notify(.display_tabbar(true))
}
.ignoresSafeArea(.all)
}
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
ScrollView(showsIndicators: false) {
HStack {
Text(NSLocalizedString(title, comment: "Section title for type of relay server list"))
.font(.system(size: 32, weight: .bold))
Spacer()
if state.keypair.privkey != nil {
Button(action: {
show_add_relay.toggle()
}) {
HStack {
Text(verbatim: "Add relay")
.padding(10)
}
}
.buttonStyle(NeutralButtonStyle())
}
}
.padding(.top, 5)
ForEach(relayList, id: \.url) { relay in
Group {
RelayView(state: state, relay: relay.url.id, showActionButtons: $showActionButtons, recommended: recommended)
Divider()
}
}
Spacer()
.padding(25)
}
.padding(.horizontal)
}
}
extension RelayConfigView{
func CustomTabItem(title: String, isActive: Bool) -> some View {
HStack {
Text(title)
.font(.system(size: 12, weight: isActive ? .bold : .regular))
.foregroundColor(isActive ? .damusAdaptableBlack : .damusAdaptableBlack.opacity(0.7))
}
.frame(width: 110, height: 30)
.background(isActive ? .damusAdaptableWhite.opacity(0.9) : .clear)
.cornerRadius(30)
}
}

View File

@ -66,97 +66,101 @@ struct RelayDetailView: View {
}
var body: some View {
Group {
Form {
if let keypair = state.keypair.to_full() {
if check_connection() {
RemoveRelayButton(keypair)
} else {
Button(action: {
guard let ev_before_add = state.contacts.event else {
return
NavigationView {
ZStack {
Group {
Form {
if let keypair = state.keypair.to_full() {
if check_connection() {
RemoveRelayButton(keypair)
} else {
Button(action: {
guard let ev_before_add = state.contacts.event else {
return
}
guard let relay_url = RelayURL(relay),
let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay_url, info: .rw) else {
return
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
dismiss()
}) {
Text("Connect To Relay", comment: "Button to connect to the relay.")
}
}
guard let relay_url = RelayURL(relay),
let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay_url, info: .rw) else {
return
}
if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
authentication_state != .none {
Section(NSLocalizedString("Authentication", comment: "Header label to display authentication details for a given relay.")) {
RelayAuthenticationDetail(state: authentication_state)
}
}
if let pubkey = nip11?.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserViewRow(damus_state: state, pubkey: pubkey)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
}
}
}
if let relay_connection {
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
HStack {
Text(relay)
Spacer()
RelayStatusView(connection: relay_connection)
}
}
}
if let nip11 {
if nip11.is_paid {
Section(content: {
RelayPaidDetail(payments_url: nip11.payments_url)
}, header: {
Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.")
}, footer: {
Text("This is a paid relay, you must pay for notes to be accepted.", comment: "Footer description that explains that the relay server requires payment to post.")
})
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
Section(NSLocalizedString("Description", comment: "Label to display relay description.")) {
FieldText(nip11.description)
}
dismiss()
}) {
Text("Connect To Relay", comment: "Button to connect to the relay.")
}
}
}
if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
authentication_state != .none {
Section(NSLocalizedString("Authentication", comment: "Header label to display authentication details for a given relay.")) {
RelayAuthenticationDetail(state: authentication_state)
}
}
if let pubkey = nip11?.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserViewRow(damus_state: state, pubkey: pubkey)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
Section(NSLocalizedString("Contact", comment: "Label to display relay contact information.")) {
FieldText(nip11.contact)
}
Section(NSLocalizedString("Software", comment: "Label to display relay software.")) {
FieldText(nip11.software)
}
Section(NSLocalizedString("Version", comment: "Label to display relay software version.")) {
FieldText(nip11.version)
}
if let nips = nip11.supported_nips, nips.count > 0 {
Section(NSLocalizedString("Supported NIPs", comment: "Label to display relay's supported NIPs.")) {
Text(nipsList(nips: nips))
}
}
}
}
if let relay_connection {
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
HStack {
Text(relay)
Spacer()
RelayStatusView(connection: relay_connection)
}
}
}
if let nip11 {
if nip11.is_paid {
Section(content: {
RelayPaidDetail(payments_url: nip11.payments_url)
}, header: {
Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.")
}, footer: {
Text("This is a paid relay, you must pay for notes to be accepted.", comment: "Footer description that explains that the relay server requires payment to post.")
})
}
Section(NSLocalizedString("Description", comment: "Label to display relay description.")) {
FieldText(nip11.description)
}
Section(NSLocalizedString("Contact", comment: "Label to display relay contact information.")) {
FieldText(nip11.contact)
}
Section(NSLocalizedString("Software", comment: "Label to display relay software.")) {
FieldText(nip11.software)
}
Section(NSLocalizedString("Version", comment: "Label to display relay software version.")) {
FieldText(nip11.version)
}
if let nips = nip11.supported_nips, nips.count > 0 {
Section(NSLocalizedString("Supported NIPs", comment: "Label to display relay's supported NIPs.")) {
Text(nipsList(nips: nips))
if state.settings.developer_mode {
Section(NSLocalizedString("Log", comment: "Label to display developer mode logs.")) {
Text(log.contents ?? NSLocalizedString("No logs to display", comment: "Label to indicate that there are no developer mode logs available to be displayed on the screen"))
.font(.system(size: 13))
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
if state.settings.developer_mode {
Section(NSLocalizedString("Log", comment: "Label to display developer mode logs.")) {
Text(log.contents ?? NSLocalizedString("No logs to display", comment: "Label to indicate that there are no developer mode logs available to be displayed on the screen"))
.font(.system(size: 13))
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
.onReceive(handle_notify(.switched_timeline)) { notif in
@ -164,6 +168,9 @@ struct RelayDetailView: View {
}
.navigationTitle(nip11?.name ?? relay)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.ignoresSafeArea(.all)
}
private func nipsList(nips: [Int]) -> AttributedString {

View File

@ -61,7 +61,7 @@ struct InnerRelayPicView: View {
}
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay(RoundedRectangle(cornerRadius: 15).stroke(failedImage ? .gray : highlight_color(highlight), lineWidth: failedImage ? 1 : pfp_line_width(highlight)))
.overlay(RoundedRectangle(cornerRadius: 15).stroke(.gray.opacity(0.5), lineWidth: 0.5))
}
}

View File

@ -51,7 +51,6 @@ struct RelayStatusView: View {
)
}
}
.padding(.trailing, 20)
}
}

View File

@ -10,22 +10,31 @@ import SwiftUI
struct RelayView: View {
let state: DamusState
let relay: String
let recommended: Bool
@ObservedObject private var model_cache: RelayModelCache
@State var relay_state: Bool
@Binding var showActionButtons: Bool
init(state: DamusState, relay: String, showActionButtons: Binding<Bool>) {
init(state: DamusState, relay: String, showActionButtons: Binding<Bool>, recommended: Bool) {
self.state = state
self.relay = relay
self.recommended = recommended
self.model_cache = state.relay_model_cache
_showActionButtons = showActionButtons
let relay_state = RelayView.get_relay_state(pool: state.pool, relay: relay)
self._relay_state = State(initialValue: relay_state)
}
static func get_relay_state(pool: RelayPool, relay: String) -> Bool {
return pool.get_relay(relay) == nil
}
var body: some View {
Group {
HStack {
if let privkey = state.keypair.privkey {
if showActionButtons {
if showActionButtons && !recommended {
RemoveButton(privkey: privkey, showText: false)
}
}
@ -39,39 +48,60 @@ struct RelayView: View {
Text(meta?.name ?? relay)
.font(.headline)
.padding(.bottom, 2)
.lineLimit(1)
RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
}
Text(relay)
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(1)
.contextMenu {
CopyAction(relay: relay)
if let privkey = state.keypair.privkey {
RemoveButton(privkey: privkey, showText: true)
}
}
}
Spacer()
if recommended {
if let keypair = state.keypair.to_full() {
VStack(alignment: .center) {
if relay_state {
AddButton(keypair: keypair)
} else {
Button(action: {
remove_action(privkey: keypair.privkey)
}) {
Text(NSLocalizedString("Added", comment: "Button to show relay server is already added to list."))
.font(.caption)
}
.buttonStyle(NeutralButtonShape.capsule.style)
.opacity(0.5)
}
}
.padding(.horizontal, 5)
}
} else {
if let relay_connection {
RelayStatusView(connection: relay_connection)
}
if let relay_connection {
RelayStatusView(connection: relay_connection)
.background(
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta), label: {
EmptyView()
})
.buttonStyle(.plain)
.disabled(showActionButtons)
)
Image("chevron-large-right")
.resizable()
.frame(width: 15, height: 15)
.foregroundColor(.gray)
}
}
.contentShape(Rectangle())
}
.swipeActions {
if let privkey = state.keypair.privkey {
RemoveButton(privkey: privkey, showText: false)
.tint(.red)
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relay_state = RelayView.get_relay_state(pool: state.pool, relay: self.relay)
}
.contextMenu {
CopyAction(relay: relay)
if let privkey = state.keypair.privkey {
RemoveButton(privkey: privkey, showText: true)
}
.onTapGesture {
state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata))
}
}
@ -79,6 +109,52 @@ struct RelayView: View {
state.pool.get_relay(relay)?.connection
}
func add_action(keypair: FullKeypair) {
guard let ev_before_add = state.contacts.event else {
return
}
guard let relay_url = RelayURL(relay),
let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay_url, info: .rw) else {
return
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
}
func remove_action(privkey: Privkey) {
guard let ev = state.contacts.event else {
return
}
let descriptors = state.pool.our_descriptors
guard let keypair = state.keypair.to_full(),
let relay_url = RelayURL(relay),
let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay_url) else {
return
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
}
func AddButton(keypair: FullKeypair) -> some View {
Button(action: {
add_action(keypair: keypair)
}) {
Text(NSLocalizedString("Add", comment: "Button to add relay server to list."))
.font(.caption)
}
.buttonStyle(NeutralButtonShape.capsule.style)
}
func CopyAction(relay: String) -> some View {
Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
@ -89,23 +165,7 @@ struct RelayView: View {
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
Button(action: {
guard let ev = state.contacts.event else {
return
}
let descriptors = state.pool.our_descriptors
guard let keypair = state.keypair.to_full(),
let relay_url = RelayURL(relay),
let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay_url) else {
return
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
remove_action(privkey: privkey)
}) {
if showText {
Text(NSLocalizedString("Disconnect", comment: "Button to disconnect from a relay server."))
@ -122,6 +182,6 @@ struct RelayView: View {
struct RelayView_Previews: PreviewProvider {
static var previews: some View {
RelayView(state: test_damus_state, relay: "wss://relay.damus.io", showActionButtons: .constant(false))
RelayView(state: test_damus_state, relay: "wss://relay.damus.io", showActionButtons: .constant(false), recommended: false)
}
}

View File

@ -0,0 +1,31 @@
//
// QuoteRepostsView.swift
// damus
//
// Created by William Casarin on 2024-03-16.
//
import SwiftUI
struct QuoteRepostsView: View {
let damus_state: DamusState
@ObservedObject var model: EventsModel
var body: some View {
TimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:))
.navigationBarTitle(NSLocalizedString("Quotes", comment: "Navigation bar title for Quote Reposts view."))
.onAppear {
model.subscribe()
}
.onDisappear {
model.unsubscribe()
}
}
}
struct QuoteRepostsView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state
QuoteRepostsView(damus_state: state, model: .reposts(state: state, target: test_note.id))
}
}

View File

@ -9,12 +9,12 @@ import SwiftUI
struct RepostsView: View {
let damus_state: DamusState
@StateObject var model: RepostsModel
@StateObject var model: EventsModel
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.events, id: \.id) { ev in
ForEach(model.events.events, id: \.id) { ev in
RepostView(damus_state: damus_state, repost: ev)
}
}
@ -33,6 +33,6 @@ struct RepostsView: View {
struct RepostsView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state
RepostsView(damus_state: state, model: RepostsModel(state: state, target: test_note.id))
RepostsView(damus_state: state, model: .reposts(state: state, target: test_note.id))
}
}

View File

@ -28,12 +28,9 @@ struct UserRelaysView: View {
var body: some View {
List(relay_state, id: \.0) { (r, add) in
RecommendedRelayView(damus: state, relay: r, add_button: add, user_recommended: true)
RelayView(state: state, relay: r, showActionButtons: .constant(true), recommended: true)
}
.listStyle(PlainListStyle())
.onReceive(handle_notify(.relays_changed)) { _ in
self.relay_state = UserRelaysView.make_relay_state(pool: state.pool, relays: self.relays)
}
.navigationBarTitle(NSLocalizedString("Relays", comment: "Navigation bar title that shows the list of relays for a user."))
}
}

View File

@ -12,14 +12,15 @@ struct ConnectWalletView: View {
@ObservedObject var model: WalletModel
@State var scanning: Bool = false
@State private var showAlert = false
@State var error: String? = nil
@State var wallet_scan_result: WalletScanResult = .scanning
var nav: NavigationCoordinator
var body: some View {
MainContent
.navigationTitle(NSLocalizedString("Attach a Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet."))
.navigationBarTitleDisplayMode(.large)
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet."))
.navigationBarTitleDisplayMode(.inline)
.padding()
.onChange(of: wallet_scan_result) { res in
scanning = false
@ -30,18 +31,29 @@ struct ConnectWalletView: View {
self.model.new(url)
case .failed:
error = NSLocalizedString("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided.")
showAlert.toggle()
case .scanning:
error = nil
}
}
.alert(isPresented: $showAlert) {
Alert(
title: Text(NSLocalizedString("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided.")),
message: Text("Make sure the wallet you are connecting to supports NWC."),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label indicating user wants to proceed."))) {
wallet_scan_result = .scanning
}
)
}
}
func AreYouSure(nwc: WalletConnectURL) -> some View {
VStack {
Text("Are you sure you want to attach this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
.font(.title)
VStack(spacing: 25) {
Text("Are you sure you want to connect this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(nwc.relay.id)
.font(.body)
@ -53,26 +65,73 @@ struct ConnectWalletView: View {
.foregroundColor(.gray)
}
BigButton(NSLocalizedString("Attach", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
Button(action: {
model.connect(nwc)
}) {
HStack {
Text("Connect", comment: "Text for button to conect to Nostr Wallet Connect lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
BigButton(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.")) {
Button(action: {
model.cancel()
}) {
HStack {
Text(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet."))
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(NeutralButtonStyle())
}
}
var ConnectWallet: some View {
VStack {
VStack(spacing: 25) {
AlbyButton() {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
}
BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
nav.push(route: Route.WalletScanner(result: $wallet_scan_result))
MutinyButton() {
openURL(URL(string:"https://app.mutinywallet.com/settings/connections")!)
}
Button(action: {
if let pasted_nwc = UIPasteboard.general.string {
guard let url = WalletConnectURL(str: pasted_nwc) else {
wallet_scan_result = .failed
return
}
wallet_scan_result = .success(url)
}
}) {
HStack {
Image("clipboard")
Text("Paste NWC Address", comment: "Text for button to connect a lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
Button(action: {
nav.push(route: Route.WalletScanner(result: $wallet_scan_result))
}) {
HStack {
Image("qr-code")
Text("Scan NWC Address", comment: "Text for button to connect a lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
if let err = self.error {
Text(err)
.foregroundColor(.red)
@ -80,14 +139,54 @@ struct ConnectWalletView: View {
}
}
var TopSection: some View {
HStack(spacing: 0) {
Button(action: {}, label: {
Image("damus-home")
.resizable()
.frame(width: 30, height: 30)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
Image("chevron-double-right")
.resizable()
.frame(width: 25, height: 25)
Button(action: {}, label: {
Image("wallet")
.resizable()
.frame(width: 30, height: 30)
.foregroundStyle(LINEAR_GRADIENT)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
}
}
var TitleSection: some View {
VStack(spacing: 25) {
Text("Damus Wallet")
.fontWeight(.bold)
Text("Securely connect your Damus app to your wallet\nusing Nostr Wallet Connect")
.font(.caption)
.multilineTextAlignment(.center)
}
}
var MainContent: some View {
Group {
TopSection
switch model.connect_state {
case .new(let nwc):
AreYouSure(nwc: nwc)
case .existing:
Text(verbatim: "Shouldn't happen")
case .none:
TitleSection
ConnectWallet
}
}

View File

@ -45,41 +45,6 @@ enum WalletScanResult: Equatable {
case scanning
}
struct NWCPaste: View {
@Binding var result: WalletScanResult
@Environment(\.colorScheme) var colorScheme
init(result: Binding<WalletScanResult>) {
self._result = result
}
var body: some View {
Button(action: {
if let pasted_nwc = UIPasteboard.general.string {
guard let url = WalletConnectURL(str: pasted_nwc) else {
self.result = .failed
return
}
self.result = .success(url)
}
}) {
HStack {
Image(systemName: "doc.on.clipboard")
Text("Paste", comment: "Button to paste a Nostr Wallet Connect string to connect the wallet for use in Damus for zaps.")
}
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(colorScheme == .light ? DamusColors.black : DamusColors.white, lineWidth: 2)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
}
}
}
struct WalletScannerView: View {
@Binding var result: WalletScanResult
@ -92,6 +57,7 @@ struct WalletScannerView: View {
case .success(let success):
guard let url = WalletConnectURL(str: success.string) else {
result = .failed
dismiss()
return
}
@ -102,8 +68,6 @@ struct WalletScannerView: View {
dismiss()
}
NWCPaste(result: $result)
.padding(.vertical)
}
}
}

View File

@ -26,19 +26,58 @@ struct WalletView: View {
Spacer()
}
Text(verbatim: nwc.relay.id)
if let lud16 = nwc.lud16 {
Text(verbatim: lud16)
VStack(spacing: 5) {
VStack(spacing: 10) {
Text("Wallet Relay")
.fontWeight(.semibold)
.padding(.top)
Divider()
RelayView(state: damus_state, relay: nwc.relay.id, showActionButtons: .constant(false), recommended: false)
}
.frame(maxWidth: .infinity, minHeight: 125, alignment: .top)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
if let lud16 = nwc.lud16 {
VStack(spacing: 10) {
Text("Wallet Address")
.fontWeight(.semibold)
Divider()
Text(verbatim: lud16)
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
}
}
BigButton(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")) {
Button(action: {
self.model.disconnect()
}) {
HStack {
Text(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet."))
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
}
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))
.navigationBarTitleDisplayMode(.large)
.navigationBarTitleDisplayMode(.inline)
.padding()
}

View File

@ -226,6 +226,22 @@
<string>Reposts</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Quote</string>
<key>other</key>
<string>Quotes</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -49,7 +49,8 @@ func generate_test_damus_state(
nav: .init(),
music: .init(onChange: {_ in }),
video: .init(),
ndb: ndb)
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey) )
return damus
}

View File

@ -280,6 +280,13 @@ extension NdbNote {
return kind == 1 || kind == 42 || kind == 30023
}
var is_quote_repost: NoteId? {
guard kind == 1, let quoted_note_id = referenced_quote_ids.first else {
return nil
}
return quoted_note_id.note_id
}
var known_kind: NostrKind? {
return NostrKind.init(rawValue: kind)
}