Compare commits

..

134 Commits

Author SHA1 Message Date
tyiu 16f6bbb5be Disable post button when media upload in progress
Changelog-Fixed: Disable post button when media upload in progress
2023-06-25 20:27:10 -04:00
tyiu 422167f7aa Add indication of followers you know in a profile
Changelog-Added: Add indication of followers you know in a profile
2023-06-25 09:38:57 +02:00
William Casarin de84456a57 mediaurl: fix is_img returning true for videos
This makes the image preloader try to download videos... not good.
2023-06-25 09:38:46 +02:00
William Casarin b70bf1f647 v1.5-6 2023-06-25 09:38:46 +02:00
William Casarin 3db77a16a0 Fix timeline from moving when you're scrolling 2023-06-24 17:39:24 +02:00
William Casarin 2256e2e625 v1.5-5 changelog 2023-06-24 08:39:51 +02:00
William Casarin a641f972ff v1.5-5 2023-06-24 08:38:02 +02:00
William Casarin c5846008f2 fix weird quirk with universe toolbar filter button 2023-06-24 08:27:12 +02:00
William Casarin 62bf767be5 nozaps: fix some tests 2023-06-23 20:46:27 +02:00
William Casarin c00746758e nozaps: disable donation star unless you're on freedom edition 2023-06-23 20:46:27 +02:00
William Casarin a89f90d7ee nozaps: hide SupportDamus on appstore builds 2023-06-23 20:46:27 +02:00
William Casarin 07d0818ee8 nozaps: disable zap delay on appstore since we don't have 1-tap anymore 2023-06-23 20:46:27 +02:00
William Casarin 0ce7414488 nozaps: disable donation zaps on appstore 2023-06-23 20:46:27 +02:00
William Casarin f090596067 nozaps: switch to global sheet when zapping
This fixes many popping bugs

Changelog-Fixed: Fix zap sheet popping
2023-06-23 20:46:27 +02:00
William Casarin 61b3ad2990 nozaps: update zap sheet style to make it clear its a user zap 2023-06-23 20:46:27 +02:00
William Casarin 8b24befaf7 nozaps: don't include zaps in thread replies 2023-06-23 20:46:27 +02:00
William Casarin 57789de5cd nozaps: hide zap total 2023-06-23 20:46:27 +02:00
William Casarin 62c539afbf nozaps: never show orange button 2023-06-23 20:46:27 +02:00
William Casarin b53e6db96b nozaps: show the user you are zapping in CustomizeZapView
This should make it clear that it's definitely not a note zap
2023-06-23 20:46:27 +02:00
William Casarin b0d6d33573 nozaps: hide zap details on notes for now 2023-06-23 20:46:27 +02:00
William Casarin c5b0e539d8 nozaps: don't show note zaps in notifications
apple sucks
2023-06-23 20:46:27 +02:00
William Casarin 601fa49a6e nozaps: don't show top zaps or zap replies
We can't associate zaps with notes anymore
2023-06-23 20:46:27 +02:00
William Casarin 216029410b nozaps: add nozaps setting
This will be used to restore functionality in the future
2023-06-23 20:46:27 +02:00
William Casarin a5b2a5c8b9 tests: disable invalid tests 2023-06-23 20:46:27 +02:00
William Casarin 980394bf0b Revert "ui: remove nip05 badge on events"
This reverts commit d205be3e0a.
2023-06-23 20:46:17 +02:00
William Casarin ed73899e5b Revert "Initial actionbar model refactor"
This reverts commit d0eb86dfa3.
2023-06-23 12:04:54 +02:00
William Casarin d0eb86dfa3 Initial actionbar model refactor 2023-06-23 11:53:53 +02:00
William Casarin 337c4de337 reduce ContentView redraws
Remove observability from the home model, and use inner models for
updating specific parts of the UI, such as notification dots on the tab
bar.
2023-06-23 11:51:51 +02:00
William Casarin e885f38c54 refactor: switch CustomizeZapView to use a model
Changelog-Fixed: Fix CustomizeZapView from randomly disappearing
2023-06-23 11:51:51 +02:00
William Casarin 3dbdc42d8b view/refactor: remove extra spacer 2023-06-23 11:46:42 +02:00
William Casarin 1389e50b8e fix some warnings 2023-06-23 11:46:42 +02:00
William Casarin 092d84f499 eventholder: don't push view updates on queued events 2023-06-23 11:46:42 +02:00
William Casarin c6a226fff8 Revert "ping: switch to async style"
This was causing crashes =/
2023-06-23 11:46:42 +02:00
William Casarin e023d1e9cb settings: turn off wallet selector by default
This is a bit confusing for new users
2023-06-23 11:46:42 +02:00
William Casarin e6b8e39106 actionbar: rename accessibility label boosts to reposts 2023-06-23 11:46:42 +02:00
William Casarin ced028755c debug: InnerTimeline render counts 2023-06-23 11:46:42 +02:00
William Casarin dabf737654 Merge remote-tracking branch 'github/translations' 2023-06-22 10:25:14 +02:00
William Casarin 72d141af61 wording: send a "message" with the zap, not "reply" 2023-06-22 10:23:56 +02:00
William Casarin 4d43e590e0 view: Add ZapUserView
This will be used to make it clear that we are zapping a user
2023-06-22 10:23:56 +02:00
William Casarin c413589582 eventgroup: add is_note_zap
We'll need this to hide note zaps as per apple's insanely dumb
guidelines.
2023-06-22 10:23:56 +02:00
William Casarin c218e0dcdd refactor: use zap.is_anon instead of recomputing
I believe this code was written before we computed is_anon inside the
zap.
2023-06-22 10:23:56 +02:00
William Casarin 892765eaa5 UserView: Make spacer optional
We need this to center this view sometimes. We should look into removing
this in the future?
2023-06-22 10:23:56 +02:00
William Casarin 455f1f7e1f view/refactor: remove redundant view structs 2023-06-22 10:23:56 +02:00
William Casarin 797762e7d2 view/refactor: move sheet handler in CustomizeZapView 2023-06-22 10:23:56 +02:00
transifex-integration[bot] efe6689bfb Translate Localizable.stringsdict in ja
100% translated source file: 'Localizable.stringsdict'
on 'ja'.
2023-06-22 01:14:40 +00:00
transifex-integration[bot] e30541c37e Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2023-06-22 01:12:38 +00:00
transifex-integration[bot] b126257d05 Translate Localizable.stringsdict in es_ES
100% translated source file: 'Localizable.stringsdict'
on 'es_ES'.
2023-06-21 23:22:27 +00:00
transifex-integration[bot] fde21559c7 Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2023-06-21 23:15:17 +00:00
transifex-integration[bot] d551b5f28b Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2023-06-21 23:14:48 +00:00
transifex-integration[bot] 961ff6f28b Translate Localizable.strings in fr
100% translated source file: 'Localizable.strings'
on 'fr'.
2023-06-21 15:20:57 +00:00
transifex-integration[bot] fdfd0f0275 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2023-06-21 15:16:19 +00:00
transifex-integration[bot] 8b1b597f2a Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2023-06-21 15:16:05 +00:00
transifex-integration[bot] f3de41ff08 Translate Localizable.stringsdict in vi
100% translated source file: 'Localizable.stringsdict'
on 'vi'.
2023-06-21 14:33:19 +00:00
transifex-integration[bot] bf010be27a Translate Localizable.strings in vi
100% translated source file: 'Localizable.strings'
on 'vi'.
2023-06-21 14:32:11 +00:00
transifex-integration[bot] b013c1f1fd Translate Localizable.stringsdict in zh_HK
100% translated source file: 'Localizable.stringsdict'
on 'zh_HK'.
2023-06-21 12:37:01 +00:00
transifex-integration[bot] 50dfa9e2ed Translate Localizable.stringsdict in zh_TW
100% translated source file: 'Localizable.stringsdict'
on 'zh_TW'.
2023-06-21 12:36:51 +00:00
transifex-integration[bot] 78450792cb Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2023-06-21 12:36:17 +00:00
transifex-integration[bot] 4dc2571177 Translate Localizable.strings in zh_HK
100% translated source file: 'Localizable.strings'
on 'zh_HK'.
2023-06-21 12:35:58 +00:00
transifex-integration[bot] 04493b53dc Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-06-21 12:35:14 +00:00
transifex-integration[bot] f383388f42 Translate Localizable.stringsdict in zh_CN
100% translated source file: 'Localizable.stringsdict'
on 'zh_CN'.
2023-06-21 12:34:30 +00:00
transifex-integration[bot] d4cdc7706d Translate Localizable.stringsdict in fr
100% translated source file: 'Localizable.stringsdict'
on 'fr'.
2023-06-21 11:57:54 +00:00
tyiu b70406d669 Fix "zapped your profile" strings to say "zapped you"
Changelog-Fixed: Fix "zapped your profile" strings to say "zapped you"
2023-06-21 07:35:16 -04:00
William Casarin a2866ff6b3 Merge remote-tracking branch 'github/translations' 2023-06-21 10:19:47 +02:00
transifex-integration[bot] 1f0e31faa0 Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2023-06-21 08:14:48 +00:00
William Casarin 2ff12cdfa6 video: switch videoplayer to use detached tasks 2023-06-20 17:00:48 +02:00
William Casarin b7b7d65612 perf: don't use string concat when calculing SeenEven hash
Profiler was complaining about this one
2023-06-20 17:00:48 +02:00
William Casarin d205be3e0a ui: remove nip05 badge on events
Changelog-Changed: Remove nip05 on events
2023-06-20 17:00:48 +02:00
William Casarin 0bea81c632 perf: move blurhash processing to a background task
Using Task directly will only inherent the parent thread. We need
detached to do heaving processing.
2023-06-20 17:00:48 +02:00
William Casarin e4842cca3c logs: don't print filters
It can be slow
2023-06-20 17:00:48 +02:00
William Casarin 87d4752aa4 ui: use AboutView in existing views 2023-06-20 17:00:48 +02:00
William Casarin 6ec533b0cd view: Add AboutView
This will be used by different views for the user's about section
2023-06-20 17:00:48 +02:00
William Casarin 51a58360f9 debug slow scroll 2023-06-20 15:29:44 +02:00
William Casarin fe025532e8 Merge remote-tracking branch 'github/translations' 2023-06-20 11:27:29 +02:00
William Casarin 6eb548a0a9 Fix reconnect loop issues on iOS17
Changelog-Fixed: Fix reconnect loop issues on iOS17
2023-06-20 11:21:21 +02:00
William Casarin bcaa1d2354 ping: switch to async style
because reasons
2023-06-20 11:21:07 +02:00
Bryan Montz 296d96d6df rename RelayStatus to RelayStatusView 2023-06-20 10:18:34 +02:00
Bryan Montz 28854fdc93 simplify and fix issues with RelayStatus 2023-06-20 10:18:34 +02:00
Bryan Montz 2901cc860f make RelayConnection's state observable 2023-06-20 10:18:34 +02:00
William Casarin 3db13ae171 Fix build for iOS17 2023-06-20 10:18:23 +02:00
transifex-integration[bot] 49dedaec04 Translate Localizable.strings in sv_SE
100% translated source file: 'Localizable.strings'
on 'sv_SE'.
2023-06-20 07:50:51 +00:00
transifex-integration[bot] 491d4c4d25 Translate Localizable.strings in sv_SE
100% translated source file: 'Localizable.strings'
on 'sv_SE'.
2023-06-20 07:49:38 +00:00
transifex-integration[bot] afcbaea331 Translate Localizable.strings in zh_HK
100% translated source file: 'Localizable.strings'
on 'zh_HK'.
2023-06-19 10:21:11 +00:00
transifex-integration[bot] 340e134046 Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2023-06-19 10:21:08 +00:00
transifex-integration[bot] e68952fa0c Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-06-19 10:20:07 +00:00
transifex-integration[bot] 83af4ddd89 Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-06-19 10:19:29 +00:00
transifex-integration[bot] 53262afa01 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2023-06-19 08:39:13 +00:00
transifex-integration[bot] 95148e9c9c Translate Localizable.strings in cs
100% translated source file: 'Localizable.strings'
on 'cs'.
2023-06-19 08:04:14 +00:00
transifex-integration[bot] 5bf4a2cc5a Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2023-06-19 07:30:18 +00:00
tyiu 2d29403145 Remove string referencing tipping posts, add missing localized string comments, and export strings for translation 2023-06-19 00:39:35 -04:00
gladiusKatana df20b67fc1 video: stop video in post when it disappears from view
Changelog-Fixes: Stop video when it disappears from view
2023-06-16 18:34:14 +02:00
William Casarin 0b5d68c0b8 Revert "threads: attempt to fix state jankiness"
This reverts commit f4024895ba.
2023-06-14 09:30:58 +02:00
William Casarin f4024895ba threads: attempt to fix state jankiness
Changelog-Fixed: Fix some more thread jankiness
2023-06-14 09:20:51 +02:00
William Casarin bcdd0b4e23 Revert "Add Inter font"
This reverts commit 271e3ad54a.
2023-06-12 11:25:27 +02:00
William Casarin 1c655d47b2 home: add comment explaining send_home_filters 2023-06-10 15:09:43 +02:00
William Casarin 24cc361d60 refactor: make send_initial_filter more readible 2023-06-10 15:09:43 +02:00
Bryan Montz 71bb9d6c92 Add text description to WebSocket for state logging 2023-06-10 12:39:25 +02:00
Ben Weeks 271e3ad54a Add Inter font
ChangeLog-Changed: Switch to new font (Inter)
2023-06-10 12:38:13 +02:00
transifex-integration[bot] dac21a1562 Update Translations
Translate Localizable.strings in cs
Translate Localizable.strings in el_GR
Translate Localizable.strings in nl
Translate Localizable.strings in sv_SE
Translate Localizable.strings in zh_CN
Translate Localizable.strings in zh_HK
Translate Localizable.strings in zh_TW
Translate Localizable.stringsdict in el_GR
Translate Localizable.stringsdict in nl
Translate Localizable.stringsdict in sv_SE
Translate Localizable.stringsdict in zh_CN
Translate Localizable.stringsdict in zh_HK
Translate Localizable.stringsdict in zh_TW

Closes: #1262
2023-06-10 11:43:32 +02:00
tyiu ae9ae66b39 Fix spelling of Nostr to use Titlecase instead of lowercase
Changelog-Fixed: Fix spelling of Nostr to use Titlecase instead of lowercase
2023-06-10 11:42:25 +02:00
tyiu e7281fdacc Add missing localized string comment 2023-06-10 11:42:25 +02:00
tyiu baa5454e2a Rename all usages of the term Post as a noun to Note to conform to the Nostr spec
Changelog-Fixed: Rename all usages of the term Post as a noun to Note to conform to the Nostr spec
2023-06-10 11:42:25 +02:00
gladiusKatana 60a892d73b LoginView: prevent explainer-text cutoff on login with npub
Changelog-Fixed: Fix text cutoff on login with npub
Closes: #1264
2023-06-10 11:39:00 +02:00
William Casarin 0ee360f2fa Fix video player hangs
Changelog-Fixed: Fix hangs due to video player
2023-06-10 11:33:35 +02:00
William Casarin c59d2a96af Make profile picture placeholder gray instead of purple 2023-06-10 11:33:18 +02:00
William Casarin ba3a6b07b2 Fix warnings 2023-06-10 11:33:07 +02:00
William Casarin 043eb5b436 Show zap comments in threads and show top zap
Changelog-Added: Top zaps
Changelog-Added: Show zap comments in threads
2023-06-09 10:11:25 +02:00
William Casarin 8f237b47eb qrscan: use explicit types when scanning 2023-06-07 08:06:06 +02:00
William Casarin a0caf9ce07 find_event: refactor with more explicit types 2023-06-07 08:06:06 +02:00
William Casarin 3277aac220 refactor: use guard in qr profile lookup 2023-06-07 08:06:03 +02:00
William Casarin e67dac13c6 refactor: use guard in handleProfileScan 2023-06-07 08:06:03 +02:00
Suhail Saqan 5f2c8223bd Add qr code scanner
Changelog-Added: Add qr code scanner
Closes: #733
2023-06-07 08:05:27 +02:00
tyiu 14977fe3dd Replace indexed mentions with NIP-27
Changelog-Fixed: Replace indexed mentions with NIP-27
Closes: #1213
2023-06-07 06:22:38 +02:00
William Casarin 1d3c181b85 Translations
Translate Localizable.strings in ja
Translate Localizable.strings in de
Translate Localizable.strings in vi
Translate Localizable.strings in zh_TW
Translate Localizable.strings in zh_HK
Translate Localizable.strings in zh_CN
Translate Localizable.strings in ru
Localizable.strings in cs

Closes: #1253
2023-06-07 06:17:30 +02:00
tyiu 8ca377bec9 Add max length truncation to displayed profile attributes to mitigate spam
Changelog-Fixed: Add max length truncation to displayed profile attributes to mitigate spam
Fixes: #1237
2023-06-04 17:50:52 -04:00
tyiu 952d6746d5 Add profile zaps
Refactor profile zaps to reuse same BOLT11 Lightning invoice logic as
note zaps, which fixes profile zaps from Cash App and Muun wallets

Changelog-Added: Add profile zaps
Changelog-Fixed: Fix profile zapping for Muun and Strike wallets
Closes: #1236
Fixes: #1067
2023-06-04 10:56:43 -07:00
Suhail Saqan b3b335f917 Add NWC paste button
Changelog-Added: Added Wallet Connect paste button
Closes: #1235
2023-06-04 10:56:43 -07:00
tyiu dde48132c9 Fix CI tests
Closes: #1204
2023-05-31 16:40:59 -07:00
William Casarin 809a08ef63 v1.5 (3) 2023-05-30 19:40:37 -07:00
William Casarin 57e6f083b8 Revert "Updated UI to use custom font"
This reverts commit 020a00bf7e.
2023-05-30 19:35:21 -07:00
William Casarin cfa1e13887 Revert "Add .frame & .position modifiers to TextEntry using ScrollView geometry"
This reverts commit f7a0370824.
2023-05-30 19:29:23 -07:00
William Casarin 6eecb5ef26 video-player: don't stop audio, mix with external media 2023-05-30 18:44:36 -07:00
William Casarin 5dc3e2635e video-player: don't randomly stop video 2023-05-30 18:38:04 -07:00
William Casarin 2713e76e17 v1.5-2 changelog 2023-05-30 18:08:57 -07:00
William Casarin 6aa28fce6c v1.5-2 2023-05-30 18:04:21 -07:00
William Casarin a6fb175b98 Add Full-Bleed Video Player
Changelog-Added: Add new full-bleed video player
2023-05-30 18:02:19 -07:00
William Casarin 554c091d57 video-player: hide mute button when we have no audio 2023-05-30 17:58:48 -07:00
William Casarin 9e359650bf carousel: fix image positioning 2023-05-30 17:58:48 -07:00
William Casarin bb091d072f cache: move event-specific media metadata to EventCache 2023-05-30 17:58:48 -07:00
William Casarin 88b04fde09 xcode: remove .git ext from package 2023-05-30 17:58:48 -07:00
William Casarin a1753b2c24 video-player: add tap gesture to prevent nav 2023-05-30 17:58:48 -07:00
William Casarin 80fac1903e carousel: switch to media carousel and include video 2023-05-30 09:36:39 -07:00
William Casarin 6214ab8d8f video: add DamusVideoPlayer view 2023-05-29 17:11:14 -07:00
William Casarin 85cd1bea19 urls: combine url classification 2023-05-29 17:11:13 -07:00
William Casarin 4d95d36a1e Add GSPlayer + VideoPlayer 2023-05-29 17:10:14 -07:00
134 changed files with 2817 additions and 1424 deletions
+50
View File
@@ -1,3 +1,53 @@
## [1.5-5] - 2023-06-24
### Fixed
- Remove note zaps to fit apples appstore guidelines
- Fix zap sheet popping (William Casarin)
- Fix CustomizeZapView from randomly disappearing (William Casarin)
- Fix "zapped your profile" strings to say "zapped you" (Terry Yiu)
- Fix reconnect loop issues on iOS17 (William Casarin)
- Fix some more thread jankiness (William Casarin)
- Fix spelling of Nostr to use Titlecase instead of lowercase (Terry Yiu)
- Rename all usages of the term Post as a noun to Note to conform to the Nostr spec (Terry Yiu)
- Fix text cutoff on login with npub (gladiusKatana)
- Fix hangs due to video player (William Casarin)
[1.5-5]: https://github.com/damus-io/damus/releases/tag/v1.5-5
## [1.5-2] - 2023-05-30
### Added
- Add new full-bleed video player (William Casarin)
- Add ability to show multiple posts per user in Universe (Ben Weeks)
- Custom iconography added for other areas of the app. (Ben Weeks)
- Custom iconography for the left navigation. (Ben Weeks)
- Custom iconography for the tab buttons. (Ben Weeks)
- Added dots under image carousel (Ben Weeks)
- Add profile caching (Bryan Montz)
- Add mention parsing and fine-grained text selection on description in ProfileView (Terry Yiu)
### Changed
- Redesign phase 1 (text, icons)
- Updated UI to use custom font (Ben Weeks)
### Fixed
- Fix side menu bug in landscape (OlegAba)
- Use "Follow me on nostr" text when looking at someone else's QR code (Ben Weeks)
- Fix issue where cursor dissapears when typing long message (gladiusKatana)
- Attempt fix for randomly broken animated gifs (William Casarin)
- Fix cursor jumping when pressing return (gladius)
- Fix side menu label size so that translations in longer languages fit without wrapping (Terry Yiu)
- Fix reaction notification title to be consistent with ReactionView (Terry Yiu)
- Fix nostr URL scheme to open properly even if there's already a different view open (Terry Yiu)
- Fix crash related to preloading events (Bryan Montz)
## v1.4.3 - 2023-05-08 ## v1.4.3 - 2023-05-08
### Added ### Added
+75 -46
View File
@@ -11,13 +11,16 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; }; 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; }; 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; }; 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; }; 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; }; 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; }; 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -49,6 +52,7 @@
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; }; 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; }; 4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; }; 4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; }; 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
@@ -128,6 +132,7 @@
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; }; 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; }; 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
@@ -166,6 +171,8 @@
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; }; 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */; };
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; }; 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
@@ -193,6 +200,7 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; }; 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; }; 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; }; 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; }; 4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; }; 4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
@@ -207,6 +215,8 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; }; 4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; }; 4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */; };
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; }; 4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
@@ -234,7 +244,7 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; }; 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; }; 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; }; 4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */; };
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; }; 4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; }; 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; }; 4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
@@ -295,14 +305,6 @@
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E4AE2AC92A227E6000680283 /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC82A227E6000680283 /* LICENSE.txt */; };
E4AE2ACA2A227E7800680283 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC62A227E4200680283 /* Inter-Regular.otf */; };
E4AE2ACB2A227E7E00680283 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC52A227E3600680283 /* Inter-Bold.otf */; };
E4AE2ACC2A227E8300680283 /* Inter-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC72A227E5400680283 /* Inter-Light.otf */; };
E4AE2ACE2A2286D000680283 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4AE2ACD2A2286D000680283 /* FontManager.swift */; };
E4AE2AD32A228CB400680283 /* Inter-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */; };
E4AE2AD42A228CB900680283 /* Inter-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AD22A228CA700680283 /* Inter-Medium.otf */; };
E4AE2AD52A228CC000680283 /* Inter-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */; };
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
@@ -342,6 +344,7 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonModel.swift; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -367,6 +370,7 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; }; 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -384,6 +388,7 @@
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -459,6 +464,7 @@
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; }; 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; }; 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; }; 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayer.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; }; 4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; }; 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; }; 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
@@ -567,6 +573,7 @@
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; }; 4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; }; 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; }; 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; }; 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; }; 4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -610,6 +617,8 @@
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 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>"; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; };
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; }; 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
@@ -637,6 +646,7 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; }; 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; }; 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; }; 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; }; 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; }; 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
@@ -651,6 +661,7 @@
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; }; 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; }; 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; }; 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; }; 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; }; 4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
@@ -681,7 +692,7 @@
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; }; 4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; }; 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; }; 4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; }; 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusView.swift; sourceTree = "<group>"; };
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; }; 4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; }; 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; }; 4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
@@ -743,14 +754,6 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; 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>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E4AE2AC52A227E3600680283 /* Inter-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Bold.otf"; sourceTree = "<group>"; };
E4AE2AC62A227E4200680283 /* Inter-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Regular.otf"; sourceTree = "<group>"; };
E4AE2AC72A227E5400680283 /* Inter-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Light.otf"; sourceTree = "<group>"; };
E4AE2AC82A227E6000680283 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
E4AE2ACD2A2286D000680283 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Italic.otf"; sourceTree = "<group>"; };
E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-SemiBold.otf"; sourceTree = "<group>"; };
E4AE2AD22A228CA700680283 /* Inter-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Medium.otf"; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
@@ -771,6 +774,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -872,6 +876,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = { 4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C9AA1462A444422003F49FD /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */, 4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
@@ -913,6 +918,7 @@
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */, 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */, 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
4C7D09772A0B0CC900943473 /* WalletModel.swift */, 4C7D09772A0B0CC900943473 /* WalletModel.swift */,
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -949,6 +955,15 @@
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4C1A9A2829DDF53B00516EAC /* Video */ = {
isa = PBXGroup;
children = (
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */,
);
path = Video;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = { 4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -965,6 +980,7 @@
children = ( children = (
4C54AA0929A55429003E4487 /* EventGroup.swift */, 4C54AA0929A55429003E4487 /* EventGroup.swift */,
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */, 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */,
); );
path = Notifications; path = Notifications;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -975,6 +991,7 @@
4C7D09692A0AEA0400943473 /* CodeScanner */, 4C7D09692A0AEA0400943473 /* CodeScanner */,
4C7D095A2A098C5C00943473 /* Wallet */, 4C7D095A2A098C5C00943473 /* Wallet */,
4C8D1A6D29F31E4100ACDF75 /* Buttons */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */,
4C1A9A2829DDF53B00516EAC /* Video */,
4C1A9A1B29DDCF8B00516EAC /* Settings */, 4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */, 4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */, 4CCEB7AC29B53D180078AA28 /* Search */,
@@ -1137,7 +1154,7 @@
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */, 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */, 50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */, 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
E4AE2ACD2A2286D000680283 /* FontManager.swift */, 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1151,6 +1168,14 @@
path = Buttons; path = Buttons;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4C9AA1462A444422003F49FD /* Zaps */ = {
isa = PBXGroup;
children = (
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = { 4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1160,7 +1185,7 @@
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */, 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */, F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */, 4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */, 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */,
4CE879512996B68900F758CC /* RelayType.swift */, 4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */, 4CDA128929E9D10C0006FA5A /* SignalView.swift */,
); );
@@ -1191,6 +1216,7 @@
4CB9D4A52992D01900A9A7E4 /* Profile */ = { 4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CB8FC222A41ABA500763C51 /* AboutView.swift */,
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */, F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
@@ -1202,6 +1228,7 @@
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */, 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */, 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */, 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */,
); );
path = Profile; path = Profile;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1295,7 +1322,6 @@
4CE6DEE527F7A08100C66700 /* damus */ = { 4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E40B46262A2272E6005E70FD /* Fonts */,
F7F0BA23297892AE009531F3 /* Modifiers */, F7F0BA23297892AE009531F3 /* Modifiers */,
4C4A3A5A288A1B2200453788 /* damus.entitlements */, 4C4A3A5A288A1B2200453788 /* damus.entitlements */,
4CE4F9DF285287A000C00DD9 /* Components */, 4CE4F9DF285287A000C00DD9 /* Components */,
@@ -1385,6 +1411,7 @@
4CE879572996C45300F758CC /* ZapsView.swift */, 4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */,
); );
path = Zaps; path = Zaps;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1452,20 +1479,6 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E40B46262A2272E6005E70FD /* Fonts */ = {
isa = PBXGroup;
children = (
E4AE2AC52A227E3600680283 /* Inter-Bold.otf */,
E4AE2AC62A227E4200680283 /* Inter-Regular.otf */,
E4AE2AC72A227E5400680283 /* Inter-Light.otf */,
E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */,
E4AE2AD22A228CA700680283 /* Inter-Medium.otf */,
E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */,
E4AE2AC82A227E6000680283 /* LICENSE.txt */,
);
path = Fonts;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = { F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1501,6 +1514,7 @@
packageProductDependencies = ( packageProductDependencies = (
4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */, 4C06670328FC7EC500038D2A /* Kingfisher */,
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */,
); );
productName = damus; productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -1605,6 +1619,7 @@
packageReferences = ( packageReferences = (
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
); );
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -1622,17 +1637,10 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E4AE2AD52A228CC000680283 /* Inter-Italic.otf in Resources */,
E4AE2AD42A228CB900680283 /* Inter-Medium.otf in Resources */,
E4AE2AD32A228CB400680283 /* Inter-SemiBold.otf in Resources */,
E4AE2ACC2A227E8300680283 /* Inter-Light.otf in Resources */,
E4AE2ACB2A227E7E00680283 /* Inter-Bold.otf in Resources */,
E4AE2ACA2A227E7800680283 /* Inter-Regular.otf in Resources */,
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */, 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */, 4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */, 3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */, 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */,
E4AE2AC92A227E6000680283 /* LICENSE.txt in Resources */,
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */, 4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
4C198DF129F88C6B004C165C /* License.txt in Resources */, 4C198DF129F88C6B004C165C /* License.txt in Resources */,
4C198DF029F88C6B004C165C /* Readme.md in Resources */, 4C198DF029F88C6B004C165C /* Readme.md in Resources */,
@@ -1669,6 +1677,7 @@
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
@@ -1687,6 +1696,7 @@
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
@@ -1753,7 +1763,6 @@
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */, 4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
E4AE2ACE2A2286D000680283 /* FontManager.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */, 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */, 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */, F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
@@ -1772,6 +1781,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */, 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */, 4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
@@ -1811,9 +1821,11 @@
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */, F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */, 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */, 4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
@@ -1848,6 +1860,7 @@
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */, 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
@@ -1895,6 +1908,7 @@
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */, E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */, 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
@@ -1909,11 +1923,12 @@
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */, 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */, 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */, 4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */, 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */,
@@ -1925,6 +1940,7 @@
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */, 4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
@@ -2220,7 +2236,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2268,7 +2284,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2441,6 +2457,14 @@
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9; revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
}; };
}; };
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/wxxsw/GSPlayer";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.26;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -2454,6 +2478,11 @@
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
productName = secp256k1; productName = secp256k1;
}; };
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = {
isa = XCSwiftPackageProductDependency;
package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */;
productName = GSPlayer;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */
@@ -1,5 +1,14 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "gsplayer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/wxxsw/GSPlayer",
"state" : {
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
"version" : "0.2.26"
}
},
{ {
"identity" : "kingfisher", "identity" : "kingfisher",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
+5 -3
View File
@@ -24,9 +24,11 @@ struct GradientButtonStyle: ButtonStyle {
struct GradientButtonStyle_Previews: PreviewProvider { struct GradientButtonStyle_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VStack { VStack {
Button("Dynamic Size", action: { Button(action: {
print("dynamic size") print("dynamic size")
}) }) {
Text(verbatim: "Dynamic Size")
}
.buttonStyle(GradientButtonStyle()) .buttonStyle(GradientButtonStyle())
@@ -34,7 +36,7 @@ struct GradientButtonStyle_Previews: PreviewProvider {
print("infinite width") print("infinite width")
}) { }) {
HStack { HStack {
Text("Infinite Width") Text(verbatim: "Infinite Width")
} }
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
} }
+90 -55
View File
@@ -53,8 +53,9 @@ enum ImageShape {
} }
// MARK: - Image Carousel // MARK: - Image Carousel
@MainActor
struct ImageCarousel: View { struct ImageCarousel: View {
var urls: [URL] var urls: [MediaUrl]
let evid: String let evid: String
@@ -69,11 +70,13 @@ struct ImageCarousel: View {
@State private var firstImageHeight: CGFloat? = nil @State private var firstImageHeight: CGFloat? = nil
@State private var currentImageHeight: CGFloat? @State private var currentImageHeight: CGFloat?
@State private var selectedIndex = 0 @State private var selectedIndex = 0
@State private var video_size: CGSize? = nil
init(state: DamusState, evid: String, urls: [URL]) { init(state: DamusState, evid: String, urls: [MediaUrl]) {
_open_sheet = State(initialValue: false) _open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil) _current_url = State(initialValue: nil)
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid)) let media_model = state.events.get_cache_data(evid).media_metadata_model
_image_fill = State(initialValue: media_model.fill)
self.urls = urls self.urls = urls
self.evid = evid self.evid = evid
self.state = state self.state = state
@@ -102,77 +105,108 @@ struct ImageCarousel: View {
} }
} }
.onAppear { .onAppear {
if self.image_fill == nil, if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
let meta = state.events.lookup_img_metadata(url: url),
let size = meta.meta.dim?.size
{
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight) let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
self.image_fill = fill self.image_fill = fill
} }
} }
} }
var Images: some View { func video_model(_ url: URL) -> VideoPlayerModel {
return state.events.get_video_player_model(url: url)
}
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
Group {
switch url {
case .image(let url):
Img(geo: geo, url: url, index: index)
.onTapGesture {
open_sheet = true
}
case .video(let url):
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
.onChange(of: video_size) { size in
guard let size else { return }
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
print("video_size changed \(size)")
if self.image_fill == nil {
print("video_size firstImageHeight \(fill.height)")
firstImageHeight = fill.height
state.events.get_cache_data(evid).media_metadata_model.fill = fill
}
self.image_fill = fill
}
}
}
}
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.events.get_cache_data(evid).media_metadata_model.fill = fill
// blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen
// earlier than this (by the preloader, etc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
state.events.lookup_img_metadata(url: url)?.state = .not_needed
}
image_fill = fill
if index == 0 {
firstImageHeight = fill.height
//maxHeight = firstImageHeight ?? maxHeight
} else {
//maxHeight = firstImageHeight ?? fill.height
}
}
.background {
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.position(x: geo.size.width / 2, y: geo.size.height / 2)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.padding(0)
}
var Medias: some View {
TabView(selection: $selectedIndex) { TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in ForEach(urls.indices, id: \.self) { index in
let url = urls[index]
GeometryReader { geo in GeometryReader { geo in
KFAnimatedImage(url) Media(geo: geo, url: urls[index], index: index)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.previews.cache_image_meta(evid: evid, image_fill: fill)
// blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen
// earlier than this (by the preloader, etc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
state.events.lookup_img_metadata(url: url)?.state = .not_needed
}
image_fill = fill
if index == 0 {
firstImageHeight = fill.height
//maxHeight = firstImageHeight ?? maxHeight
} else {
//maxHeight = firstImageHeight ?? fill.height
}
}
.background {
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.padding(0)
} }
} }
} }
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $open_sheet) { .fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls, disable_animation: state.settings.disable_animation) ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
} }
.frame(height: height) .frame(height: height)
.onTapGesture {
open_sheet = true
}
.onChange(of: selectedIndex) { value in .onChange(of: selectedIndex) { value in
selectedIndex = value selectedIndex = value
} }
.tabViewStyle(PageTabViewStyle()) .tabViewStyle(PageTabViewStyle())
} }
var body: some View { var body: some View {
VStack { VStack {
Images Medias
.onTapGesture { }
// This is our custom carousel image indicator // This is our custom carousel image indicator
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex) CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
@@ -181,8 +215,8 @@ struct ImageCarousel: View {
} }
// MARK: - Custom Carousel // MARK: - Custom Carousel
struct CarouselDotsView: View { struct CarouselDotsView<T>: View {
let urls: [URL] let urls: [T]
@Binding var selectedIndex: Int @Binding var selectedIndex: Int
var body: some View { var body: some View {
@@ -254,7 +288,8 @@ public struct ImageFill {
// MARK: - Preview Provider // MARK: - Preview Provider
struct ImageCarousel_Previews: PreviewProvider { struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!]) let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
} }
} }
+5 -4
View File
@@ -37,7 +37,7 @@ struct InvoiceView: View {
var PayButton: some View { var PayButton: some View {
Button { Button {
if settings.show_wallet_selector { if settings.show_wallet_selector {
showing_select_wallet = true present_sheet(.select_wallet(invoice: invoice.string))
} else { } else {
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
} }
@@ -79,9 +79,6 @@ struct InvoiceView: View {
} }
.padding(30) .padding(30)
} }
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
} }
} }
@@ -116,3 +113,7 @@ struct InvoiceView_Previews: PreviewProvider {
} }
} }
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet, sheet)
}
+1 -1
View File
@@ -18,7 +18,7 @@ struct Reposted: View {
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false) ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).") Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
} }
} }
+1 -11
View File
@@ -12,7 +12,7 @@ struct TruncatedText: View {
let maxChars: Int = 280 let maxChars: Int = 280
var body: some View { var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString() let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
if let truncatedAttributedString { if let truncatedAttributedString {
Text(truncatedAttributedString) Text(truncatedAttributedString)
@@ -28,16 +28,6 @@ struct TruncatedText: View {
.allowsHitTesting(false) .allowsHitTesting(false)
} }
} }
func getTruncatedString() -> AttributedString? {
let nsAttributedString = NSAttributedString(text.attributed)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
} }
struct TruncatedText_Previews: PreviewProvider { struct TruncatedText_Previews: PreviewProvider {
+14 -6
View File
@@ -32,9 +32,17 @@ struct UserViewRow: View {
struct UserView: View { struct UserView: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String let pubkey: String
let spacer: Bool
@State var about_text: Text? = nil
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
self.damus_state = damus_state
self.pubkey = pubkey
self.spacer = spacer
}
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
@@ -42,16 +50,16 @@ struct UserView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false) ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
if let about = profile?.about { if let about_text {
let blocks = parse_mentions(content: about, tags: []) about_text
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
Text(about_string)
.lineLimit(3) .lineLimit(3)
.font(.footnote) .font(.footnote)
} }
} }
Spacer() if spacer {
Spacer()
}
} }
} }
} }
+2
View File
@@ -23,6 +23,8 @@ struct WebsiteLink: View {
Text(link_text) Text(link_text)
.font(.footnote) .font(.footnote)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.truncationMode(.tail)
.lineLimit(1)
}) })
} }
} }
+53 -62
View File
@@ -10,39 +10,38 @@ import SwiftUI
enum ZappingEventType { enum ZappingEventType {
case failed(ZappingError) case failed(ZappingError)
case got_zap_invoice(String) case got_zap_invoice(String)
case sent_from_nwc
} }
enum ZappingError { enum ZappingError {
case fetching_invoice case fetching_invoice
case bad_lnurl case bad_lnurl
case canceled
case send_failed
} }
struct ZappingEvent { struct ZappingEvent {
let is_custom: Bool let is_custom: Bool
let type: ZappingEventType let type: ZappingEventType
let event: NostrEvent let target: ZapTarget
}
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
@Published var showing_select_wallet: Bool = false
@Published var showing_zap_customizer: Bool = false
} }
struct ZapButton: View { struct ZapButton: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let target: ZapTarget
let lnurl: String let lnurl: String
@ObservedObject var zaps: ZapsDataModel @ObservedObject var zaps: ZapsDataModel
@StateObject var button: ZapButtonModel = ZapButtonModel()
var our_zap: Zapping? { var our_zap: Zapping? {
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey }) zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey })
} }
var zap_img: String { var zap_img: String {
if damus_state.settings.nozaps {
return "zap"
}
switch our_zap { switch our_zap {
case .none: case .none:
return "zap" return "zap"
@@ -54,24 +53,21 @@ struct ZapButton: View {
} }
var zap_color: Color { var zap_color: Color {
if damus_state.settings.nozaps {
return Color.gray
}
if our_zap == nil { if our_zap == nil {
return Color.gray return Color.gray
} }
// always orange ! // always orange !
return Color.orange return Color.orange
/*
if our_zap.is_paid {
return Color.orange
} else {
return Color.yellow
}
*/
} }
func tap() { func tap() {
guard let our_zap else { guard let our_zap else {
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
return return
} }
@@ -128,7 +124,7 @@ struct ZapButton: View {
.frame(width:20, height: 20) .frame(width:20, height: 20)
}) })
if zaps.zap_total > 0 { if !damus_state.settings.nozaps && zaps.zap_total > 0 {
Text(verbatim: format_msats_abbrev(zaps.zap_total)) Text(verbatim: format_msats_abbrev(zaps.zap_total))
.font(.footnote) .font(.footnote)
.foregroundColor(zap_color) .foregroundColor(zap_color)
@@ -136,41 +132,22 @@ struct ZapButton: View {
} }
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
.simultaneousGesture(LongPressGesture().onEnded {_ in .simultaneousGesture(LongPressGesture().onEnded {_ in
button.showing_zap_customizer = true // when we don't have nozaps mode enable, long press shows the zap customizer
if !damus_state.settings.nozaps {
present_sheet(.zap(target: target, lnurl: lnurl))
}
// long press does nothing in nozaps mode
}) })
.highPriorityGesture(TapGesture().onEnded { .highPriorityGesture(TapGesture().onEnded {
tap() // when we have appstore mode on, only show the zap customizer as "user zaps"
if damus_state.settings.nozaps {
present_sheet(.zap(target: target, lnurl: lnurl))
} else {
// otherwise we restore the original behavior of one-tap zaps
tap()
}
}) })
.sheet(isPresented: $button.showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state.settings.show_wallet_selector {
self.button.invoice = inv
self.button.showing_select_wallet = true
} else {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
}
}
} }
} }
@@ -180,7 +157,7 @@ struct ZapButton_Previews: PreviewProvider {
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
let zaps = ZapsDataModel([.pending(pending_zap)]) let zaps = ZapsDataModel([.pending(pending_zap)])
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps) ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
} }
} }
@@ -196,14 +173,13 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
return .external(ExtPendingZapState(state: .fetching_invoice)) return .external(ExtPendingZapState(state: .fetching_invoice))
} }
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let keypair = damus_state.keypair.to_full() else { guard let keypair = damus_state.keypair.to_full() else {
return return
} }
// Only take the first 10 because reasons // Only take the first 10 because reasons
let relays = Array(damus_state.pool.our_descriptors.prefix(10)) let relays = Array(damus_state.pool.our_descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? "" let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -231,7 +207,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async { DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.bad_lnurl) let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev) notify(.zapping, ev)
} }
return return
@@ -245,7 +221,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async { DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice) let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev) notify(.zapping, ev)
} }
return return
@@ -258,24 +234,35 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
// don't both continuing, user has canceled // don't both continuing, user has canceled
if case .cancel_fetching_invoice = nwc_state.state { if case .cancel_fetching_invoice = nwc_state.state {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.canceled)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return return
} }
var flusher: OnFlush? = nil var flusher: OnFlush? = nil
// Don't donate on custom zaps
if !is_custom && damus_state.settings.donation_percent > 0 { // donations are only enabled on one-tap zaps and off appstore
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
flusher = .once({ pe in flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task.init { @MainActor in Task { @MainActor in
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
} }
}) })
} }
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: flusher) // we don't have a delay on one-tap nozaps (since this will be from customize zap view)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return return
} }
@@ -284,9 +271,13 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
if pzap_state.update_state(state: .postbox_pending(nwc_req)) { if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here // we don't need to trigger a ZapsDataModel update here
} }
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
notify(.zapping, ev)
case .external(let pending_ext): case .external(let pending_ext):
pending_ext.state = .done pending_ext.state = .done
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event) let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
notify(.zapping, ev) notify(.zapping, ev)
} }
} }
+135 -35
View File
@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AVKit
struct TimestampedProfile { struct TimestampedProfile {
let profile: Profile let profile: Profile
@@ -13,17 +14,38 @@ struct TimestampedProfile {
let event: NostrEvent let event: NostrEvent
} }
struct ZapSheet {
let target: ZapTarget
let lnurl: String
}
struct SelectWallet {
let invoice: String
}
enum Sheets: Identifiable { enum Sheets: Identifiable {
case post(PostAction) case post(PostAction)
case report(ReportTarget) case report(ReportTarget)
case event(NostrEvent) case event(NostrEvent)
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter case filter
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
}
static func select_wallet(invoice: String) -> Sheets {
return .select_wallet(SelectWallet(invoice: invoice))
}
var id: String { var id: String {
switch self { switch self {
case .report: return "report" case .report: return "report"
case .post(let action): return "post-" + (action.ev?.id ?? "") case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id case .event(let ev): return "event-" + ev.id
case .zap(let sheet): return "zap-" + sheet.target.id
case .select_wallet: return "select-wallet"
case .filter: return "filter" case .filter: return "filter"
} }
} }
@@ -74,7 +96,7 @@ struct ContentView: View {
@State var confirm_overwrite_mutelist: Bool = false @State var confirm_overwrite_mutelist: Bool = false
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies @SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel() var home: HomeModel = HomeModel()
let sub_id = UUID().description let sub_id = UUID().description
@@ -114,8 +136,8 @@ struct ContentView: View {
.safeAreaInset(edge: .top, spacing: 0) { .safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) { VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: { CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts) Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies) Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
}) })
Divider() Divider()
.frame(height: 1) .frame(height: 1)
@@ -127,7 +149,7 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack { ZStack {
if let damus = self.damus_state { if let damus = self.damus_state {
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter) TimelineView(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
} }
} }
} }
@@ -288,13 +310,14 @@ struct ContentView: View {
if selected_timeline == .search { if selected_timeline == .search {
Button(action: { Button(action: {
//isFilterVisible.toggle() //isFilterVisible.toggle()
self.active_sheet = .filter present_sheet(.filter)
}) { }) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease // checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter") Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
.foregroundColor(.gray) .foregroundColor(.gray)
//.contentShape(Rectangle()) //.contentShape(Rectangle())
} }
.buttonStyle(.plain)
} }
} }
} }
@@ -307,7 +330,7 @@ struct ContentView: View {
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline) TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8) .padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea()) .background(Color(uiColor: .systemBackground).ignoresSafeArea())
} }
@@ -315,6 +338,7 @@ struct ContentView: View {
.ignoresSafeArea(.keyboard) .ignoresSafeArea(.keyboard)
.onAppear() { .onAppear() {
self.connect() self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
setup_notifications() setup_notifications()
} }
.sheet(item: $active_sheet) { item in .sheet(item: $active_sheet) { item in
@@ -325,6 +349,10 @@ struct ContentView: View {
PostView(action: action, damus_state: damus_state!) PostView(action: action, damus_state: damus_state!)
case .event: case .event:
EventDetailView() EventDetailView()
case .zap(let zapsheet):
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
case .select_wallet(let select):
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
case .filter: case .filter:
let timeline = selected_timeline let timeline = selected_timeline
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
@@ -432,6 +460,31 @@ struct ContentView: View {
.onReceive(handle_notify(.unmute_thread)) { notif in .onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_events() home.filter_events()
} }
.onReceive(handle_notify(.present_sheet)) { notif in
let sheet = notif.object as! Sheets
self.active_sheet = sheet
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state!.settings.show_wallet_selector {
present_sheet(.select_wallet(invoice: inv))
} else {
let wallet = damus_state!.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
.onChange(of: scenePhase) { (phase: ScenePhase) in .onChange(of: scenePhase) { (phase: ScenePhase) in
switch phase { switch phase {
case .background: case .background:
@@ -454,6 +507,11 @@ struct ContentView: View {
let damus_state else { let damus_state else {
return return
} }
if local.type == .profile_zap {
open_profile(id: local.event_id)
return
}
guard let target = damus_state.events.lookup(local.event_id) else { guard let target = damus_state.events.lookup(local.event_id) else {
return return
@@ -469,6 +527,9 @@ struct ContentView: View {
case .mention: fallthrough case .mention: fallthrough
case .repost: case .repost:
open_event(ev: target) open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
} }
} }
.onReceive(handle_notify(.onlyzaps_mode)) { notif in .onReceive(handle_notify(.onlyzaps_mode)) { notif in
@@ -499,7 +560,7 @@ struct ContentView: View {
}, message: { }, message: {
if let pubkey = self.muting { if let pubkey = self.muting {
let profile = damus_state!.profiles.lookup(id: pubkey) let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.") Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else { } else {
Text("User has been muted", comment: "Alert message that informs a user was d.") Text("User has been muted", comment: "Alert message that informs a user was d.")
@@ -559,7 +620,7 @@ struct ContentView: View {
}, message: { }, message: {
if let pubkey = muting { if let pubkey = muting {
let profile = damus_state?.profiles.lookup(id: pubkey) let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.") Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else { } else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
@@ -736,24 +797,57 @@ func setup_notifications() {
} }
} }
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) { struct FindEvent {
if let ev = state.events.lookup(evid) { let type: FindEventType
callback(ev) let find_from: [String]?
return
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .profile(pubkey), find_from: find_from)
}
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .event(evid), find_from: find_from)
}
}
enum FindEventType {
case profile(String)
case event(String)
}
enum FoundEvent {
case profile(Profile, NostrEvent)
case invalid_profile(NostrEvent)
case event(NostrEvent)
}
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
var filter: NostrFilter? = nil
let find_from = query_.find_from
let query = query_.type
switch query {
case .profile(let pubkey):
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
callback(.profile(profile.profile, profile.event))
return
}
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
case .event(let evid):
if let ev = state.events.lookup(evid) {
callback(.event(ev))
return
}
filter = NostrFilter(ids: [evid], limit: 1)
} }
let subid = UUID().description let subid = UUID().description
var attempts: Int = 0
var has_event = false var has_event = false
guard let filter else { return }
var filter = search_type == .event ? NostrFilter(ids: [evid]) : NostrFilter(authors: [evid])
if search_type == .profile {
filter.kinds = [.metadata]
}
filter.limit = 1
var attempts = 0
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else { guard case .nostr_event(let ev) = res else {
@@ -769,15 +863,22 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
break break
case .event(_, let ev): case .event(_, let ev):
has_event = true has_event = true
state.pool.unsubscribe(sub_id: subid) state.pool.unsubscribe(sub_id: subid)
if search_type == .profile && ev.known_kind == .metadata { switch query {
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { case .profile:
callback(ev) if ev.known_kind == .metadata {
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
guard let profile else {
callback(.invalid_profile(ev))
return
}
callback(.profile(profile, ev))
return
}
} }
} else { case .event:
callback(ev) callback(.event(ev))
} }
case .eose: case .eose:
if !has_event { if !has_event {
@@ -800,11 +901,11 @@ func timeline_name(_ timeline: Timeline?) -> String {
} }
switch timeline { switch timeline {
case .home: case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") return NSLocalizedString("Home", comment: "Navigation bar title for Home view where notes and replies appear from those who the user is following.")
case .notifications: case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.") return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
case .search: case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.") return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where notes from all connected relay servers appear.")
case .dms: case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.") return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
} }
@@ -902,10 +1003,9 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
if ref.key == "p" { if ref.key == "p" {
result(.profile(ref.ref_id)) result(.profile(ref.ref_id))
} else if ref.key == "e" { } else if ref.key == "e" {
find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in find_event(state: state, query: .event(evid: ref.ref_id)) { res in
if let ev { guard let res, case .event(let ev) = res else { return }
result(.event(ev)) result(.event(ev))
}
} }
} }
case .filter(let filt): case .filter(let filt):
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-94
View File
@@ -1,94 +0,0 @@
Copyright (c) 2016-2020 The Inter Project Authors.
"Inter" is trademark of Rasmus Andersson.
https://github.com/rsms/inter
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-11
View File
@@ -70,16 +70,5 @@
<string>Damus needs access to your camera if you want to upload photos from it</string> <string>Damus needs access to your camera if you want to upload photos from it</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string> <string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
<key>CFBundleIdentifier</key>
<string></string>
<key>UIAppFonts</key>
<array>
<string>Inter-Regular.otf</string>
<string>Inter-Bold.otf</string>
<string>Inter-Light.otf</string>
<string>Inter-SemiBold.otf</string>
<string>Inter-Medium.otf</string>
<string>Inter-Italic.otf</string>
</array>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject {
@Published var our_zap: Zapping? @Published var our_zap: Zapping?
@Published var likes: Int @Published var likes: Int
@Published var boosts: Int @Published var boosts: Int
@Published var zaps: Int @Published private(set) var zaps: Int
@Published var zap_total: Int64 @Published var zap_total: Int64
@Published var replies: Int @Published var replies: Int
+20
View File
@@ -11,6 +11,8 @@ import Foundation
class Contacts { class Contacts {
private var friends: Set<String> = Set() private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set() private var friend_of_friends: Set<String> = Set()
/// Tracks which friends are friends of a given pubkey.
private var pubkey_to_our_friends = [String : Set<String>]()
private var muted: Set<String> = Set() private var muted: Set<String> = Set()
let our_pubkey: String let our_pubkey: String
@@ -58,6 +60,10 @@ class Contacts {
func remove_friend(_ pubkey: String) { func remove_friend(_ pubkey: String) {
friends.remove(pubkey) friends.remove(pubkey)
pubkey_to_our_friends.forEach {
pubkey_to_our_friends[$0.key]?.remove(pubkey)
}
} }
func get_friend_list() -> [String] { func get_friend_list() -> [String] {
@@ -73,6 +79,15 @@ class Contacts {
for tag in contact.tags { for tag in contact.tags {
if tag.count >= 2 && tag[0] == "p" { if tag.count >= 2 && tag[0] == "p" {
friend_of_friends.insert(tag[1]) friend_of_friends.insert(tag[1])
// Exclude themself and us.
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
if pubkey_to_our_friends[tag[1]] == nil {
pubkey_to_our_friends[tag[1]] = Set<String>()
}
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
}
} }
} }
} }
@@ -96,6 +111,11 @@ class Contacts {
func follow_state(_ pubkey: String) -> FollowState { func follow_state(_ pubkey: String) -> FollowState {
return is_friend(pubkey) ? .follows : .unfollows return is_friend(pubkey) ? .follows : .unfollows
} }
/// Gets the list of pubkeys of our friends who follow the given pubkey.
func get_friended_followers(_ pubkey: String) -> [String] {
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
}
} }
func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? { func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {
+10 -1
View File
@@ -35,8 +35,17 @@ struct DamusState {
func add_zap(zap: Zapping) -> Bool { func add_zap(zap: Zapping) -> Bool {
// store generic zap mapping // store generic zap mapping
self.zaps.add_zap(zap: zap) self.zaps.add_zap(zap: zap)
let stored = self.events.store_zap(zap: zap)
// thread zaps
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
// [nozaps]: thread zaps are only available outside of the app store
replies.count_replies(ev)
events.add_replies(ev: ev)
}
// associate with events as well // associate with events as well
return self.events.store_zap(zap: zap) return stored
} }
var pubkey: String { var pubkey: String {
+143 -77
View File
@@ -23,7 +23,7 @@ struct NewEventsBits: OptionSet {
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions] static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
} }
class HomeModel: ObservableObject { class HomeModel {
// Don't trigger a user notification for events older than a certain age // Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60 static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
@@ -49,9 +49,10 @@ class HomeModel: ObservableObject {
var signal = SignalModel() var signal = SignalModel()
@Published var new_events: NewEventsBits = NewEventsBits() var notifications = NotificationsModel()
@Published var notifications = NotificationsModel() var notification_status = NotificationStatusModel()
@Published var events: EventHolder = EventHolder() var events: EventHolder = EventHolder()
var zap_button: ZapButtonModel = ZapButtonModel()
init() { init() {
self.damus_state = DamusState.empty self.damus_state = DamusState.empty
@@ -164,65 +165,38 @@ class HomeModel: ObservableObject {
} }
} }
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
damus_state.add_zap(zap: .zap(zap))
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(.zap(zap)) {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
if damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
}
}
return
}
func handle_zap_event(_ ev: NostrEvent) { func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications process_zap_event(damus_state: damus_state, ev: ev) { zapres in
guard let ptag = event_tag(ev, name: "p") else { guard case .done(let zap) = zapres else { return }
return
} guard zap.target.pubkey == self.damus_state.keypair.pubkey else {
return
}
let our_keypair = damus_state.keypair if !self.notifications.insert_zap(.zap(zap)) {
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) { return
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper) }
return
} guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else {
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return return
} }
DispatchQueue.main.async { if self.damus_state.settings.zap_vibration {
self.damus_state.profiles.zappers[ptag] = zapper // Generate zap vibration
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper) zap_vibrate(zap_amount: zap.invoice.amount)
} }
if self.damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
switch zap.target {
case .profile(let profile_id):
create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id)
case .note(let note_target):
create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id)
}
}
self.notification_status.new_events = new_bits
} }
} }
@@ -397,12 +371,12 @@ class HomeModel: ObservableObject {
/// Send the initial filters, just our contact list mostly /// Send the initial filters, just our contact list mostly
func send_initial_filters(relay_id: String) { func send_initial_filters(relay_id: String) {
var filter = NostrFilter(kinds: [.contacts], let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
limit: 1, let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
authors: [damus_state.pubkey]) pool.send(.subscribe(subscription), to: [relay_id])
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
} }
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
func send_home_filters(relay_id: String?) { func send_home_filters(relay_id: String?) {
// TODO: since times should be based on events from a specific relay // TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow // perhaps we could mark this in the relay pool somehow
@@ -412,7 +386,7 @@ class HomeModel: ObservableObject {
var contacts_filter = NostrFilter(kinds: [.metadata]) var contacts_filter = NostrFilter(kinds: [.metadata])
contacts_filter.authors = friends contacts_filter.authors = friends
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata]) var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
our_contacts_filter.authors = [damus_state.pubkey] our_contacts_filter.authors = [damus_state.pubkey]
@@ -467,7 +441,7 @@ class HomeModel: ObservableObject {
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters) notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters) dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) //print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id { if let relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
@@ -550,8 +524,8 @@ class HomeModel: ObservableObject {
@discardableResult @discardableResult
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool { func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) { if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits self.notification_status.new_events = new_bits
return true return true
} else { } else {
return false return false
@@ -583,7 +557,7 @@ class HomeModel: ObservableObject {
} }
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) { func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
self.new_events = notifs notification_status.new_events = notifs
if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification { if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message") let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
@@ -601,7 +575,7 @@ class HomeModel: ObservableObject {
if !should_debounce_dms { if !should_debounce_dms {
self.incoming_dms.append(ev) self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
got_new_dm(notifs: notifs, ev: ev) got_new_dm(notifs: notifs, ev: ev)
} }
self.incoming_dms = [] self.incoming_dms = []
@@ -611,7 +585,7 @@ class HomeModel: ObservableObject {
incoming_dms.append(ev) incoming_dms.append(ev)
dm_debouncer.debounce { [self] in dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
got_new_dm(notifs: notifs, ev: ev) got_new_dm(notifs: notifs, ev: ev)
} }
self.incoming_dms = [] self.incoming_dms = []
@@ -799,11 +773,11 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
} }
} }
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: (() -> Void)? = nil) { func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
guard_valid_event(events: events, ev: ev) { guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
completion?() completion?(nil)
return return
} }
@@ -811,7 +785,7 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
DispatchQueue.main.async { DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev) process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
completion?() completion?(profile)
} }
} }
} }
@@ -1101,13 +1075,12 @@ func zap_notification_title(_ zap: Zap) -> String {
} }
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String { func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.private_request ?? zap.request.ev let src = zap.request.ev
let anon = event_is_anonymous(ev: src) let pk = zap.is_anon ? "anon" : src.pubkey
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount) let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
if src.content.isEmpty { if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale) let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
@@ -1118,7 +1091,28 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
} }
} }
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) { func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap) content.title = zap_notification_title(zap)
@@ -1202,7 +1196,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
case .dm: case .dm:
title = displayName title = displayName
identifier = "myDMNotification" identifier = "myDMNotification"
case .zap: case .zap, .profile_zap:
// not handled here // not handled here
break break
} }
@@ -1224,3 +1218,75 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
} }
} }
enum ProcessZapResult {
case already_processed(Zap)
case done(Zap)
case failed
}
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
completion(.failed)
return
}
// just return the zap if we already have it
if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap {
completion(.already_processed(z))
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else {
completion(.failed)
return
}
damus_state.add_zap(zap: .zap(zap))
completion(.done(zap))
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
completion(.failed)
return
}
guard let lnurl = profile.lnurl else {
completion(.failed)
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
completion(.failed)
return
}
DispatchQueue.main.async {
damus_state.profiles.zappers[ptag] = zapper
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
completion(.failed)
return
}
damus_state.add_zap(zap: .zap(zap))
completion(.done(zap))
}
}
}
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? {
let our_keypair = damus_state.keypair
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return nil
}
damus_state.add_zap(zap: .zap(zap))
return zap
}
+8 -5
View File
@@ -10,7 +10,7 @@ import Foundation
enum MentionType { enum MentionType {
case pubkey case pubkey
case event case event
var ref: String { var ref: String {
switch self { switch self {
case .pubkey: case .pubkey:
@@ -495,14 +495,17 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions:
continue continue
} }
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) { if find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) != nil {
let mention = Mention(index: ind, type: mention_type, ref: ref) // Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention) let block = Block.mention(mention)
blocks.append(block) blocks.append(block)
} else { } else {
let ind = new_tags.count
new_tags.append(refid_to_tag(ref)) new_tags.append(refid_to_tag(ref))
let mention = Mention(index: ind, type: mention_type, ref: ref) // Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention) let block = Block.mention(mention)
blocks.append(block) blocks.append(block)
} }
@@ -0,0 +1,12 @@
//
// NotificationStatusModel.swift
// damus
//
// Created by William Casarin on 2023-06-23.
//
import Foundation
class NotificationStatusModel: ObservableObject {
@Published var new_events: NewEventsBits = NewEventsBits()
}
+5 -5
View File
@@ -21,12 +21,12 @@ class ZapGroup {
} }
func zap_requests() -> [NostrEvent] { func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request } zaps.map { z in z.request.ev }
} }
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for zap in zaps { for zap in zaps {
if !isIncluded(zap.request) { if !isIncluded(zap.request.ev) {
return true return true
} }
} }
@@ -35,7 +35,7 @@ class ZapGroup {
} }
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? { func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request) } let new_zaps = zaps.filter { isIncluded($0.request.ev) }
guard new_zaps.count > 0 else { guard new_zaps.count > 0 else {
return nil return nil
} }
@@ -60,8 +60,8 @@ class ZapGroup {
msat_total += zap.amount msat_total += zap.amount
if !zappers.contains(zap.request.pubkey) { if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.pubkey) zappers.insert(zap.request.ev.pubkey)
} }
return true return true
+3 -3
View File
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
} }
for zap in incoming_zaps { for zap in incoming_zaps {
pks.insert(zap.request.pubkey) pks.insert(zap.request.ev.pubkey)
} }
return Array(pks) return Array(pks)
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
changed = changed || incoming_events.count != count changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) } profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count changed = changed || profile_zaps.zaps.count != count
for el in reactions { for el in reactions {
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps { for el in zaps {
count = el.value.zaps.count count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter { el.value.zaps = el.value.zaps.filter {
isIncluded($0.request) isIncluded($0.request.ev)
} }
changed = changed || el.value.zaps.count != count changed = changed || el.value.zaps.count != count
} }
-1
View File
@@ -21,7 +21,6 @@ struct NostrPost {
} }
} }
// TODO: parse nostr:{e,p}:pubkey uris as well
func parse_post_mention_type(_ p: Parser) -> MentionType? { func parse_post_mention_type(_ p: Parser) -> MentionType? {
if parse_char(p, "@") { if parse_char(p, "@") {
return .pubkey return .pubkey
+17
View File
@@ -10,15 +10,21 @@ import Foundation
/// manages the lifetime of a thread /// manages the lifetime of a thread
class ThreadModel: ObservableObject { class ThreadModel: ObservableObject {
@Published var event: NostrEvent @Published var event: NostrEvent
let original_event: NostrEvent
var event_map: Set<NostrEvent> var event_map: Set<NostrEvent>
init(event: NostrEvent, damus_state: DamusState) { init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
self.event_map = Set() self.event_map = Set()
self.event = event self.event = event
self.original_event = event
add_event(event) add_event(event)
} }
var is_original: Bool {
return original_event.id == event.id
}
let damus_state: DamusState let damus_state: DamusState
let profiles_subid = UUID().description let profiles_subid = UUID().description
@@ -101,6 +107,10 @@ class ThreadModel: ObservableObject {
if ev.known_kind == .metadata { if ev.known_kind == .metadata {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.known_kind == .zap {
process_zap_event(damus_state: damus_state, ev: ev) { zap in
}
} else if ev.is_textlike { } else if ev.is_textlike {
self.add_event(ev) self.add_event(ev)
} }
@@ -116,3 +126,10 @@ class ThreadModel: ObservableObject {
} }
} }
func get_top_zap(events: EventCache, evid: String) -> Zapping? {
return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in
!zap.request.marked_hidden
})
}
+5 -1
View File
@@ -84,7 +84,7 @@ class UserSettingsStore: ObservableObject {
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild) @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
var default_media_uploader: MediaUploader var default_media_uploader: MediaUploader
@Setting(key: "show_wallet_selector", default_value: true) @Setting(key: "show_wallet_selector", default_value: false)
var show_wallet_selector: Bool var show_wallet_selector: Bool
@Setting(key: "left_handed", default_value: false) @Setting(key: "left_handed", default_value: false)
@@ -126,6 +126,10 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "truncate_timeline_text", default_value: false) @Setting(key: "truncate_timeline_text", default_value: false)
var truncate_timeline_text: Bool var truncate_timeline_text: Bool
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
@Setting(key: "nozaps", default_value: true)
var nozaps: Bool
@Setting(key: "truncate_mention_text", default_value: true) @Setting(key: "truncate_mention_text", default_value: true)
var truncate_mention_text: Bool var truncate_mention_text: Bool
+13
View File
@@ -0,0 +1,13 @@
//
// ZapButtonModel.swift
// damus
//
// Created by Terry Yiu on 6/1/23.
//
import Foundation
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
}
+29
View File
@@ -0,0 +1,29 @@
//
// CustomizeZapModel.swift
// damus
//
// Created by William Casarin on 2023-06-22.
//
import Foundation
class CustomizeZapModel: ObservableObject {
@Published var comment: String = ""
@Published var custom_amount: String = ""
@Published var custom_amount_sats: Int? = nil
@Published var zap_type: ZapType = .pub
@Published var invoice: String = ""
@Published var error: String? = nil
@Published var zapping: Bool = false
@Published var show_zap_types: Bool = false
init() {
}
func set_defaults(settings: UserSettingsStore) {
self.zap_type = settings.default_zap_type
self.custom_amount = String(settings.default_zap_amount)
self.custom_amount_sats = settings.default_zap_amount
}
}
+1 -6
View File
@@ -53,18 +53,13 @@ class ZapsModel: ObservableObject {
case .notice: case .notice:
break break
case .eose: case .eose:
let events = state.events.lookup_zaps(target: target).map { $0.request } let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev): case .event(_, let ev):
guard ev.kind == 9735 else { guard ev.kind == 9735 else {
return return
} }
if let zap = state.zaps.zaps[ev.id] {
state.events.store_zap(zap: zap)
return
}
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else { guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return return
} }
+2 -2
View File
@@ -100,8 +100,8 @@ class Profile: Codable {
} }
var damus_donation: Int? { var damus_donation: Int? {
get { return int("damus_donation"); } get { return int("damus_donation_v2"); }
set(s) { set_int("damus_donation", s) } set(s) { set_int("damus_donation_v2", s) }
} }
var picture: String? { var picture: String? {
+4 -15
View File
@@ -60,24 +60,13 @@ func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
if !parse_str(p, "nostr:") { if !parse_str(p, "nostr:") {
return nil return nil
} }
guard let typ = parse_nostr_ref_uri_type(p) else { guard let ref = parse_post_bech32_mention(p) else {
p.pos = start p.pos = start
return nil return nil
} }
if !parse_char(p, ":") { return ref
p.pos = start
return nil
}
guard let pk = parse_hexstr(p, len: 64) else {
p.pos = start
return nil
}
// TODO: parse relays from nostr uris
return ReferencedId(ref_id: pk, relay_id: nil, key: typ)
} }
func decode_universal_link(_ s: String) -> NostrLink? { func decode_universal_link(_ s: String) -> NostrLink? {
+8 -3
View File
@@ -37,9 +37,9 @@ public struct RelayURL: Hashable {
} }
} }
final class RelayConnection { final class RelayConnection: ObservableObject {
private(set) var isConnected = false @Published private(set) var isConnected = false
private(set) var isConnecting = false @Published private(set) var isConnecting = false
private(set) var last_connection_attempt: TimeInterval = 0 private(set) var last_connection_attempt: TimeInterval = 0
private(set) var last_pong: Date? = nil private(set) var last_pong: Date? = nil
@@ -129,6 +129,11 @@ final class RelayConnection {
} }
case .error(let error): case .error(let error):
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)") print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
let nserr = error as NSError
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
// ignore socket not connected?
return
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.isConnected = false self.isConnected = false
self.isConnecting = false self.isConnecting = false
+7 -2
View File
@@ -18,11 +18,16 @@ struct QueuedRequest {
let relay: String let relay: String
} }
struct SeenEvent: Hashable {
let relay_id: String
let evid: String
}
class RelayPool { class RelayPool {
var relays: [Relay] = [] var relays: [Relay] = []
var handlers: [RelayHandler] = [] var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = [] var request_queue: [QueuedRequest] = []
var seen: Set<String> = Set() var seen: Set<SeenEvent> = Set()
var counts: [String: UInt64] = [:] var counts: [String: UInt64] = [:]
private let network_monitor = NWPathMonitor() private let network_monitor = NWPathMonitor()
@@ -233,7 +238,7 @@ class RelayPool {
func record_seen(relay_id: String, event: NostrConnectionEvent) { func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event { if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev { if case .event(_, let nev) = ev {
let k = relay_id + nev.id let k = SeenEvent(relay_id: relay_id, evid: nev.id)
if !seen.contains(k) { if !seen.contains(k) {
seen.insert(k) seen.insert(k)
if counts[relay_id] == nil { if counts[relay_id] == nil {
+13
View File
@@ -13,6 +13,19 @@ enum WebSocketEvent {
case message(URLSessionWebSocketTask.Message) case message(URLSessionWebSocketTask.Message)
case disconnected(URLSessionWebSocketTask.CloseCode, String?) case disconnected(URLSessionWebSocketTask.CloseCode, String?)
case error(Error) case error(Error)
var description: String? {
switch self {
case .connected:
return "Connected"
case .message(_):
return "Received message"
case .disconnected(let close_code, let reason):
return "Disconnected: Close code: \(close_code), reason: \(reason ?? "unknown")"
case .error(let error):
return "Error: \(error)"
}
}
} }
final class WebSocket: NSObject, URLSessionWebSocketDelegate { final class WebSocket: NSObject, URLSessionWebSocketDelegate {
+41 -6
View File
@@ -62,7 +62,7 @@ class ZapsDataModel: ObservableObject {
} }
func confirm_nwc(reqid: String) { func confirm_nwc(reqid: String) {
guard let zap = zaps.first(where: { z in z.request.id == reqid }), guard let zap = zaps.first(where: { z in z.request.ev.id == reqid }),
case .pending(let pzap) = zap case .pending(let pzap) = zap
else { else {
return return
@@ -83,16 +83,16 @@ class ZapsDataModel: ObservableObject {
} }
func from(_ pubkey: String) -> [Zapping] { func from(_ pubkey: String) -> [Zapping] {
return self.zaps.filter { z in z.request.pubkey == pubkey } return self.zaps.filter { z in z.request.ev.pubkey == pubkey }
} }
@discardableResult @discardableResult
func remove(reqid: String) -> Bool { func remove(reqid: String) -> Bool {
guard zaps.first(where: { z in z.request.id == reqid }) != nil else { guard zaps.first(where: { z in z.request.ev.id == reqid }) != nil else {
return false return false
} }
self.zaps = zaps.filter { z in z.request.id != reqid } self.zaps = zaps.filter { z in z.request.ev.id != reqid }
return true return true
} }
} }
@@ -101,6 +101,10 @@ class RelativeTimeModel: ObservableObject {
@Published var value: String = "" @Published var value: String = ""
} }
class MediaMetaModel: ObservableObject {
@Published var fill: ImageFill? = nil
}
class EventData { class EventData {
var translations_model: TranslationModel var translations_model: TranslationModel
var artifacts_model: NoteArtifactsModel var artifacts_model: NoteArtifactsModel
@@ -108,6 +112,7 @@ class EventData {
var zaps_model : ZapsDataModel var zaps_model : ZapsDataModel
var relative_time: RelativeTimeModel = RelativeTimeModel() var relative_time: RelativeTimeModel = RelativeTimeModel()
var validated: ValidationResult var validated: ValidationResult
var media_metadata_model: MediaMetaModel
var translations: TranslateStatus { var translations: TranslateStatus {
return translations_model.state return translations_model.state
@@ -126,6 +131,7 @@ class EventData {
self.artifacts_model = .init(state: .not_loaded) self.artifacts_model = .init(state: .not_loaded)
self.zaps_model = .init(zaps) self.zaps_model = .init(zaps)
self.validated = .unknown self.validated = .unknown
self.media_metadata_model = MediaMetaModel()
self.preview_model = .init(state: .not_loaded) self.preview_model = .init(state: .not_loaded)
} }
} }
@@ -135,6 +141,7 @@ class EventCache {
private var replies = ReplyMap() private var replies = ReplyMap()
private var cancellable: AnyCancellable? private var cancellable: AnyCancellable?
private var image_metadata: [String: ImageMetadataState] = [:] private var image_metadata: [String: ImageMetadataState] = [:]
private var video_meta: [String: VideoPlayerModel] = [:]
private var event_data: [String: EventData] = [:] private var event_data: [String: EventData] = [:]
//private var thread_latest: [String: Int64] //private var thread_latest: [String: Int64]
@@ -168,6 +175,9 @@ class EventCache {
@discardableResult @discardableResult
func store_zap(zap: Zapping) -> Bool { func store_zap(zap: Zapping) -> Bool {
let data = get_cache_data(zap.target.id).zaps_model let data = get_cache_data(zap.target.id).zaps_model
if let ev = zap.event {
insert(ev)
}
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap) return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
} }
@@ -175,7 +185,7 @@ class EventCache {
switch zap.target { switch zap.target {
case .note(let note_target): case .note(let note_target):
let zaps = get_cache_data(note_target.note_id).zaps_model let zaps = get_cache_data(note_target.note_id).zaps_model
zaps.remove(reqid: zap.request.id) zaps.remove(reqid: zap.request.ev.id)
case .profile: case .profile:
// these aren't stored anywhere yet // these aren't stored anywhere yet
break break
@@ -194,6 +204,30 @@ class EventCache {
return image_metadata[url.absoluteString.lowercased()] return image_metadata[url.absoluteString.lowercased()]
} }
@MainActor
func lookup_media_size(url: URL) -> CGSize? {
if let img_meta = lookup_img_metadata(url: url) {
return img_meta.meta.dim?.size
}
return get_video_player_model(url: url).size
}
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
video_meta[url.absoluteString] = meta
}
@MainActor
func get_video_player_model(url: URL) -> VideoPlayerModel {
if let model = video_meta[url.absoluteString] {
return model
}
let model = VideoPlayerModel()
video_meta[url.absoluteString] = model
return model
}
func parent_events(event: NostrEvent) -> [NostrEvent] { func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = [] var parents: [NostrEvent] = []
@@ -257,6 +291,7 @@ class EventCache {
private func prune() { private func prune() {
events = [:] events = [:]
video_meta = [:]
event_data = [:] event_data = [:]
replies.replies = [:] replies.replies = [:]
} }
@@ -365,7 +400,7 @@ func preload_image(url: URL) {
print("Preloading image \(url.absoluteString)") print("Preloading image \(url.absoluteString)")
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
print("Preloaded image \(url.absoluteString)") print("Preloaded image \(url.absoluteString)")
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import Foundation
class EventHolder: ObservableObject, ScrollQueue { class EventHolder: ObservableObject, ScrollQueue {
private var has_event: Set<String> private var has_event: Set<String>
@Published var events: [NostrEvent] @Published var events: [NostrEvent]
@Published var incoming: [NostrEvent] var incoming: [NostrEvent]
var should_queue: Bool var should_queue: Bool
var on_queue: ((NostrEvent) -> Void)? var on_queue: ((NostrEvent) -> Void)?
+1 -1
View File
@@ -40,7 +40,7 @@ extension KFOptionSetter {
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self { func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self } guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = ImageResource(downloadURL: url, cacheKey: key) let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource() let source = imageResource.convertToSource()
options.alternativeSources = [source] options.alternativeSources = [source]
-63
View File
@@ -1,63 +0,0 @@
//
// FontManager.swift
// damus
//
// Created by Ben Weeks on 27/05/2023.
//
import Foundation
import SwiftUI
struct FontManager {
struct dynamicSize {
public static var largeTitle: CGFloat = UIFont.preferredFont(forTextStyle: .largeTitle).pointSize - 1
public static var title1: CGFloat = UIFont.preferredFont(forTextStyle: .title1).pointSize - 0
public static var title2: CGFloat = UIFont.preferredFont(forTextStyle: .title2).pointSize - 0
public static var title3: CGFloat = UIFont.preferredFont(forTextStyle: .title3).pointSize - 0
public static var body: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize - 1
public static var callout: CGFloat = UIFont.preferredFont(forTextStyle: .callout).pointSize - 1
public static var caption1: CGFloat = UIFont.preferredFont(forTextStyle: .caption1).pointSize - 1
public static var caption2: CGFloat = UIFont.preferredFont(forTextStyle: .caption2).pointSize - 1
public static var footnote: CGFloat = UIFont.preferredFont(forTextStyle: .footnote).pointSize - 1
public static var headline: CGFloat = UIFont.preferredFont(forTextStyle: .headline).pointSize - 1
public static var subheadline: CGFloat = UIFont.preferredFont(forTextStyle: .subheadline).pointSize - 1
// repeat for all the dynamic sizes
}
struct Inter {
static let familyRoot = "Inter"
static let bold = "\(familyRoot)-Bold"
static let regular = "\(familyRoot)-Regular"
static let light = "\(familyRoot)-Light"
static let medium = "\(familyRoot)-Medium"
static let semibold = "\(familyRoot)-SemiBold"
static let italic = "\(familyRoot)-Italic"
static let largeTitle: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.largeTitle)
static let title1: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title1)
static let title2: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title2)
static let title3: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title3)
static let body: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.body)
static let caption1: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.caption1)
static let caption2: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.caption2)
static let footnote: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.footnote)
static let headline: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.headline)
static let subheadline: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.subheadline)
// repeat for other sizes
}
}
extension Font {
public static var largeTitle = FontManager.Inter.largeTitle
public static var title1 = FontManager.Inter.title1
public static var title2 = FontManager.Inter.title2
public static var title3 = FontManager.Inter.title3
public static var body = FontManager.Inter.body
public static var caption1 = FontManager.Inter.caption1
public static var caption2 = FontManager.Inter.caption2
public static var footnote = FontManager.Inter.footnote
public static var headline = FontManager.Inter.headline
public static var subheadline = FontManager.Inter.subheadline
// repeat for the rest of the dynamic sizes
}
+1 -1
View File
@@ -60,7 +60,7 @@ func hashtag_str(_ htag: String) -> CompatibleText {
} }
text = Text(attributedString) text = Text(attributedString)
let img = Image("\(name)-hashtag") let img = Image("\(name)-hashtag")
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0) text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0)
} else { } else {
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = DamusColors.purple
} }
+2 -4
View File
@@ -59,17 +59,15 @@ struct ImageMetadata: Equatable {
} }
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? { func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
let res = Task.init { let res = Task.detached(priority: .low) {
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0)) let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
guard let img = UIImage.init(blurHash: blurhash, size: size) else { guard let img = UIImage.init(blurHash: blurhash, size: size) else {
let noimg: UIImage? = nil let noimg: UIImage? = nil
return noimg return noimg
} }
return img return img
} }
return await res.value return await res.value
} }
@@ -146,7 +144,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
return nil return nil
} }
let res = Task.init { let res = Task.detached(priority: .low) {
let bhs = get_blurhash_size(img_size: img.size) let bhs = get_blurhash_size(img_size: img.size)
let smaller = img.resized(to: bhs) let smaller = img.resized(to: bhs)
+2 -2
View File
@@ -11,10 +11,10 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi
var i: Int = 0 var i: Int = 0
for zap in zaps { for zap in zaps {
if new_zap.request.id == zap.request.id { if new_zap.request.ev.id == zap.request.ev.id {
// replace pending // replace pending
if !new_zap.is_pending && zap.is_pending { if !new_zap.is_pending && zap.is_pending {
print("nwc: replacing pending with real zap \(new_zap.request.id)") print("nwc: replacing pending with real zap \(new_zap.request.ev.id)")
zaps[i] = new_zap zaps[i] = new_zap
return true return true
} }
+1
View File
@@ -44,4 +44,5 @@ enum LocalNotificationType: String {
case mention case mention
case repost case repost
case zap case zap
case profile_zap
} }
+3
View File
@@ -77,6 +77,9 @@ extension Notification.Name {
static var update_stats: Notification.Name { static var update_stats: Notification.Name {
return Notification.Name("update_stats") return Notification.Name("update_stats")
} }
static var present_sheet: Notification.Name {
return Notification.Name("present_sheet")
}
static var zapping: Notification.Name { static var zapping: Notification.Name {
return Notification.Name("zapping") return Notification.Name("zapping")
} }
-10
View File
@@ -66,22 +66,12 @@ enum PreviewState {
class PreviewCache { class PreviewCache {
private var previews: [String: Preview] private var previews: [String: Preview]
private var image_meta: [String: ImageFill]
func lookup(_ evid: String) -> Preview? { func lookup(_ evid: String) -> Preview? {
return previews[evid] return previews[evid]
} }
func lookup_image_meta(_ evid: String) -> ImageFill? {
return image_meta[evid]
}
func cache_image_meta(evid: String, image_fill: ImageFill) {
self.image_meta[evid] = image_fill
}
init() { init() {
self.previews = [:] self.previews = [:]
self.image_meta = [:]
} }
} }
+34
View File
@@ -0,0 +1,34 @@
//
// StringUtil.swift
// damus
//
// Created by Terry Yiu on 6/4/23.
//
import Foundation
extension String {
/// Returns a copy of the String truncated to maxLength and "..." ellipsis appended to the end,
/// or if the String does not exceed maxLength, the String itself is returned without truncation or added ellipsis.
func truncate(maxLength: Int) -> String {
guard count > maxLength else {
return self
}
return self[...self.index(self.startIndex, offsetBy: maxLength - 1)] + "..."
}
}
extension AttributedString {
/// Returns a copy of the AttributedString truncated to maxLength and "..." ellipsis appended to the end,
/// or if the AttributedString does not exceed maxLength, nil is returned.
func truncateOrNil(maxLength: Int) -> AttributedString? {
let nsAttributedString = NSAttributedString(self)
if nsAttributedString.length < maxLength { return nil }
let range = NSRange(location: 0, length: maxLength)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
+28 -9
View File
@@ -41,7 +41,16 @@ public enum ZapTarget: Equatable {
struct ZapRequest { struct ZapRequest {
let ev: NostrEvent let ev: NostrEvent
let marked_hidden: Bool
var is_in_thread: Bool {
return !self.ev.content.isEmpty && !marked_hidden
}
init(ev: NostrEvent) {
self.ev = ev
self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0] == "hidden" }) != nil
}
} }
enum ExtPendingZapStateType { enum ExtPendingZapStateType {
@@ -129,7 +138,7 @@ struct ZapRequestId: Equatable {
let reqid: String let reqid: String
init(from_zap: Zapping) { init(from_zap: Zapping) {
self.reqid = from_zap.request.id self.reqid = from_zap.request.ev.id
} }
init(from_makezap: MakeZapRequest) { init(from_makezap: MakeZapRequest) {
@@ -198,12 +207,12 @@ enum Zapping {
} }
} }
var request: NostrEvent { var request: ZapRequest {
switch self { switch self {
case .zap(let zap): case .zap(let zap):
return zap.request_ev return zap.request
case .pending(let pzap): case .pending(let pzap):
return pzap.request.ev return pzap.request
} }
} }
@@ -227,6 +236,15 @@ enum Zapping {
} }
} }
var is_in_thread: Bool {
switch self {
case .zap(let zap):
return zap.request.is_in_thread
case .pending(let pzap):
return pzap.request.is_in_thread
}
}
var is_anon: Bool { var is_anon: Bool {
switch self { switch self {
case .zap(let zap): case .zap(let zap):
@@ -242,12 +260,12 @@ struct Zap {
public let invoice: ZapInvoice public let invoice: ZapInvoice
public let zapper: String /// zap authorizer public let zapper: String /// zap authorizer
public let target: ZapTarget public let target: ZapTarget
public let request: ZapRequest public let raw_request: ZapRequest
public let is_anon: Bool public let is_anon: Bool
public let private_request: NostrEvent? public let private_request: ZapRequest?
var request_ev: NostrEvent { var request: ZapRequest {
return private_request ?? self.request.ev return private_request ?? self.raw_request
} }
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? { public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
@@ -295,8 +313,9 @@ struct Zap {
} }
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req) let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
let preq = private_request.map { pr in ZapRequest(ev: pr) }
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request) return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, raw_request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: preq)
} }
} }
+11 -8
View File
@@ -12,8 +12,8 @@ class Zaps {
let our_pubkey: String let our_pubkey: String
var our_zaps: [String: [Zapping]] var our_zaps: [String: [Zapping]]
var event_counts: [String: Int] private(set) var event_counts: [String: Int]
var event_totals: [String: Int64] private(set) var event_totals: [String: Int64]
init(our_pubkey: String) { init(our_pubkey: String) {
self.zaps = [:] self.zaps = [:]
@@ -27,13 +27,13 @@ class Zaps {
var res: Zapping? = nil var res: Zapping? = nil
for kv in our_zaps { for kv in our_zaps {
let ours = kv.value let ours = kv.value
guard let zap = ours.first(where: { z in z.request.id == reqid }) else { guard let zap = ours.first(where: { z in z.request.ev.id == reqid }) else {
continue continue
} }
res = zap res = zap
our_zaps[kv.key] = ours.filter { z in z.request.id != reqid } our_zaps[kv.key] = ours.filter { z in z.request.ev.id != reqid }
if let count = event_counts[zap.target.id] { if let count = event_counts[zap.target.id] {
event_counts[zap.target.id] = count - 1 event_counts[zap.target.id] = count - 1
@@ -51,13 +51,16 @@ class Zaps {
} }
func add_zap(zap: Zapping) { func add_zap(zap: Zapping) {
if zaps[zap.request.id] != nil { if zaps[zap.request.ev.id] != nil {
return return
} }
self.zaps[zap.request.id] = zap self.zaps[zap.request.ev.id] = zap
if let zap_id = zap.event?.id {
self.zaps[zap_id] = zap
}
// record our zaps for an event // record our zaps for an event
if zap.request.pubkey == our_pubkey { if zap.request.ev.pubkey == our_pubkey {
switch zap.target { switch zap.target {
case .note(let note_target): case .note(let note_target):
if our_zaps[note_target.note_id] == nil { if our_zaps[note_target.note_id] == nil {
@@ -71,7 +74,7 @@ class Zaps {
} }
// don't count tips to self. lame. // don't count tips to self. lame.
guard zap.request.pubkey != zap.target.pubkey else { guard zap.request.ev.pubkey != zap.target.pubkey else {
return return
} }
+3 -3
View File
@@ -62,7 +62,7 @@ struct EventActionBar: View {
self.show_repost_action = true self.show_repost_action = true
} }
} }
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button")) .accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")") Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray) .foregroundColor(bar.boosted ? Color.green : Color.gray)
@@ -88,14 +88,14 @@ struct EventActionBar: View {
if let lnurl = self.lnurl { if let lnurl = self.lnurl {
Spacer() Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
} }
Spacer() Spacer()
EventActionButton(img: "upload", col: Color.gray) { EventActionButton(img: "upload", col: Color.gray) {
show_share_action = true show_share_action = true
} }
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post")) .accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
} }
.onAppear { .onAppear {
self.bar.update(damus: damus_state, evid: self.event.id) self.bar.update(damus: damus_state, evid: self.event.id)
+7 -5
View File
@@ -19,7 +19,11 @@ struct EventDetailBar: View {
self.target = target self.target = target
self.target_pk = target_pk self.target_pk = target_pk
self._bar = ObservedObject(wrappedValue: make_actionbar_model(ev: target, damus: state)) self._bar = ObservedObject(wrappedValue: make_actionbar_model(ev: target, damus: state))
}
var ZapDetails: Text {
let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray)
return Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
} }
var body: some View { var body: some View {
@@ -40,13 +44,11 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
if bar.zaps > 0 { if !state.settings.nozaps && bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk)) let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) { NavigationLink(destination: dst) {
let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray) ZapDetails
Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.") }.buttonStyle(PlainButtonStyle())
}
.buttonStyle(PlainButtonStyle())
} }
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@ let carousel_items = [
CarouselItem(image: Image("undercover"), CarouselItem(image: Image("undercover"),
text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")), text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")),
CarouselItem(image: Image("bitcoin-p2p"), CarouselItem(image: Image("bitcoin-p2p"),
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.")) text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friends and stack sats with Bitcoin⚡️, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string."))
] ]
struct CarouselView: View { struct CarouselView: View {
+3 -3
View File
@@ -66,7 +66,7 @@ struct CreateAccountView: View {
self.is_done = true self.is_done = true
}) { }) {
HStack { HStack {
Text("Create account now", comment: "Button to create account.") Text("Create account now", comment: "Button to create account.")
.fontWeight(.semibold) .fontWeight(.semibold)
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
@@ -96,7 +96,7 @@ struct LoginPrompt: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
HStack { HStack {
Text("Already on nostr?", comment: "Ask the user if they already have an account on nostr") Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr")
.foregroundColor(Color("DamusMediumGrey")) .foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) { Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
@@ -167,7 +167,7 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
Text(title) Text(title)
.bold() .bold()
if optional { if optional {
Text("- optional", comment: "Label indicating that a form input is optional.") Text("optional", comment: "Label indicating that a form input is optional.")
.font(.callout) .font(.callout)
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
} }
+1 -1
View File
@@ -117,7 +117,7 @@ struct EULAView: View {
.ignoresSafeArea(), .ignoresSafeArea(),
alignment: .top alignment: .top
) )
.navigationTitle("EULA") .navigationTitle(NSLocalizedString("EULA", comment: "Navigation title of view that shows the EULA, an acronym for End User License Agreement."))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav()) .navigationBarItems(leading: BackNav())
+2 -1
View File
@@ -38,8 +38,9 @@ struct EventView: View {
} }
} else if event.known_kind == .zap { } else if event.known_kind == .zap {
if let zap = damus.zaps.zaps[event.id] { if let zap = damus.zaps.zaps[event.id] {
ZapEvent(damus: damus, zap: zap) ZapEvent(damus: damus, zap: zap, is_top_zap: options.contains(.top_zap))
} else { } else {
Text("Invalid Zap", comment: "Text indicating that a zap event is malformed and could not be displayed.")
EmptyView() EmptyView()
} }
} else { } else {
+2 -2
View File
@@ -31,9 +31,9 @@ struct MutedEventView: View {
.foregroundColor(DamusColors.adaptableGrey) .foregroundColor(DamusColors.adaptableGrey)
HStack { HStack {
Text("Post from a user you've muted", comment: "Text to indicate that what is being shown is a post from a user who has been muted.") Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.")
Spacer() Spacer()
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been muted.")) { Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) {
shown.toggle() shown.toggle()
} }
} }
+1 -1
View File
@@ -39,7 +39,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
let names: [String] = pubkeys.map { let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0) let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0).username return Profile.displayName(profile: prof, pubkey: $0).username.truncate(maxLength: 50)
} }
let uniqueNames = NSOrderedSet(array: names).array as! [String] let uniqueNames = NSOrderedSet(array: names).array as! [String]
+1
View File
@@ -18,6 +18,7 @@ struct EventViewOptions: OptionSet {
static let no_translate = EventViewOptions(rawValue: 1 << 6) static let no_translate = EventViewOptions(rawValue: 1 << 6)
static let small_pfp = EventViewOptions(rawValue: 1 << 7) static let small_pfp = EventViewOptions(rawValue: 1 << 7)
static let nested = EventViewOptions(rawValue: 1 << 8) static let nested = EventViewOptions(rawValue: 1 << 8)
static let top_zap = EventViewOptions(rawValue: 1 << 9)
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
} }
+17 -7
View File
@@ -10,13 +10,23 @@ import SwiftUI
struct ZapEvent: View { struct ZapEvent: View {
let damus: DamusState let damus: DamusState
let zap: Zapping let zap: Zapping
let is_top_zap: Bool
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .center) { HStack(alignment: .center) {
Text("⚡️ \(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user") Image("zap.fill")
.foregroundColor(.orange)
Text(verbatim: format_msats(zap.amount))
.font(.headline) .font(.headline)
.padding([.top], 2)
if is_top_zap {
Text("Top Zap", comment: "Text indicating that this zap is the one with the highest amount of sats.")
.font(.caption)
.foregroundColor(.gray)
.padding([.top], 2)
}
if zap.is_private { if zap.is_private {
Image("lock") Image("lock")
@@ -31,7 +41,7 @@ struct ZapEvent: View {
} }
} }
TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to]) TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
.padding([.top], 1) .padding([.top], 1)
} }
} }
@@ -41,18 +51,18 @@ struct ZapEvent: View {
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000) let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734) let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
let test_zap_request = ZapRequest(ev: test_zap_request_ev) let test_zap_request = ZapRequest(ev: test_zap_request_ev)
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil) let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: nil)
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event) let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: .init(ev: test_event))
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
struct ZapEvent_Previews: PreviewProvider { struct ZapEvent_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VStack { VStack {
ZapEvent(damus: test_damus_state(), zap: .zap(test_zap)) ZapEvent(damus: test_damus_state(), zap: .zap(test_zap), is_top_zap: true)
ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap)) ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap), is_top_zap: false)
} }
} }
} }
+19 -2
View File
@@ -29,9 +29,27 @@ struct FollowUserView: View {
} }
} }
struct FollowersYouKnowView: View {
let damus_state: DamusState
let friended_followers: [String]
@EnvironmentObject var followers: FollowersModel
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(friended_followers, id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
}
.padding(.horizontal)
}
.navigationBarTitle(NSLocalizedString("Followers You Know", comment: "Navigation bar title for view that shows who is following a user."))
}
}
struct FollowersView: View { struct FollowersView: View {
let damus_state: DamusState let damus_state: DamusState
let whos: String
@EnvironmentObject var followers: FollowersModel @EnvironmentObject var followers: FollowersModel
@@ -58,7 +76,6 @@ struct FollowingView: View {
let damus_state: DamusState let damus_state: DamusState
let following: FollowingModel let following: FollowingModel
let whos: String
var body: some View { var body: some View {
ScrollView { ScrollView {
+15 -4
View File
@@ -10,7 +10,8 @@ import Kingfisher
struct ImageContainerView: View { struct ImageContainerView: View {
let url: URL? let cache: EventCache
let url: MediaUrl
@State private var image: UIImage? @State private var image: UIImage?
@State private var showShareSheet = false @State private var showShareSheet = false
@@ -26,8 +27,7 @@ struct ImageContainerView: View {
} }
} }
var body: some View { func Img(url: URL) -> some View {
KFAnimatedImage(url) KFAnimatedImage(url)
.imageContext(.note, disable_animation: disable_animation) .imageContext(.note, disable_animation: disable_animation)
.configure { view in .configure { view in
@@ -40,12 +40,23 @@ struct ImageContainerView: View {
ShareSheet(activityItems: [url]) ShareSheet(activityItems: [url])
} }
} }
var body: some View {
Group {
switch url {
case .image(let url):
Img(url: url)
case .video(let url):
DamusVideoPlayer(url: url, model: cache.get_video_player_model(url: url), video_size: .constant(nil))
}
}
}
} }
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")! let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
struct ImageContainerView_Previews: PreviewProvider { struct ImageContainerView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ImageContainerView(url: test_image_url, disable_animation: false) ImageContainerView(cache: test_damus_state().events, url: .image(test_image_url), disable_animation: false)
} }
} }
+5 -4
View File
@@ -8,8 +8,8 @@
import SwiftUI import SwiftUI
struct ImageView: View { struct ImageView: View {
let cache: EventCache
let urls: [URL?] let urls: [MediaUrl]
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@@ -39,7 +39,7 @@ struct ImageView: View {
TabView(selection: $selectedIndex) { TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView { ZoomableScrollView {
ImageContainerView(url: urls[index], disable_animation: disable_animation) ImageContainerView(cache: cache, url: urls[index], disable_animation: disable_animation)
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top) .padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom) .padding(.bottom, Theme.safeAreaInsets?.bottom)
@@ -79,6 +79,7 @@ struct ImageView: View {
struct ImageView_Previews: PreviewProvider { struct ImageView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false) let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageView(cache: test_damus_state().events, urls: [url], disable_animation: false)
} }
} }
+3 -2
View File
@@ -80,9 +80,10 @@ struct LoginView: View {
} }
if parsed?.is_pub ?? false { if parsed?.is_pub ?? false {
Text("This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.", comment: "Warning that the inputted account key is a public key and the result of what happens because of it.") Text("This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.", comment: "Warning that the inputted account key is a public key and the result of what happens because of it.")
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
.bold() .bold()
.fixedSize(horizontal: false, vertical: true)
} }
if let p = parsed { if let p = parsed {
@@ -339,7 +340,7 @@ struct CreateAccountPrompt: View {
@Binding var create_account: Bool @Binding var create_account: Bool
var body: some View { var body: some View {
HStack { HStack {
Text("New to nostr?", comment: "Ask the user if they are new to nostr") Text("New to Nostr?", comment: "Ask the user if they are new to Nostr")
.foregroundColor(Color("DamusMediumGrey")) .foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) { Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
+8 -8
View File
@@ -29,7 +29,7 @@ struct TabButton: View {
let timeline: Timeline let timeline: Timeline
let img: String let img: String
@Binding var selected: Timeline @Binding var selected: Timeline
@Binding var new_events: NewEventsBits @ObservedObject var nstatus: NotificationStatusModel
let settings: UserSettingsStore let settings: UserSettingsStore
let action: (Timeline) -> () let action: (Timeline) -> ()
@@ -38,7 +38,7 @@ struct TabButton: View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
Tab Tab
if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) { if show_indicator(timeline: timeline, current: nstatus.new_events, indicator_setting: settings.notification_indicators) {
Circle() Circle()
.size(CGSize(width: 8, height: 8)) .size(CGSize(width: 8, height: 8))
.frame(width: 10, height: 10, alignment: .topTrailing) .frame(width: 10, height: 10, alignment: .topTrailing)
@@ -53,7 +53,7 @@ struct TabButton: View {
Button(action: { Button(action: {
action(timeline) action(timeline)
let bits = timeline_to_notification_bits(timeline, ev: nil) let bits = timeline_to_notification_bits(timeline, ev: nil)
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue) nstatus.new_events = NewEventsBits(rawValue: nstatus.new_events.rawValue & ~bits.rawValue)
}) { }) {
Image(selected != timeline ? img : "\(img).fill") Image(selected != timeline ? img : "\(img).fill")
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -65,7 +65,7 @@ struct TabButton: View {
struct TabBar: View { struct TabBar: View {
@Binding var new_events: NewEventsBits var nstatus: NotificationStatusModel
@Binding var selected: Timeline @Binding var selected: Timeline
let settings: UserSettingsStore let settings: UserSettingsStore
@@ -75,10 +75,10 @@ struct TabBar: View {
VStack { VStack {
Divider() Divider()
HStack { HStack {
TabButton(timeline: .home, img: "home", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1") TabButton(timeline: .home, img: "home", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("1")
TabButton(timeline: .dms, img: "messages", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2") TabButton(timeline: .dms, img: "messages", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("2")
TabButton(timeline: .search, img: "search", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3") TabButton(timeline: .search, img: "search", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("3")
TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4") TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("4")
} }
} }
} }
+121 -22
View File
@@ -130,11 +130,11 @@ struct NoteContentView: View {
} }
} }
if show_images && artifacts.images.count > 0 { if show_images && artifacts.media.count > 0 {
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images) ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
} else if !show_images && artifacts.images.count > 0 { } else if !show_images && artifacts.media.count > 0 {
ZStack { ZStack {
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images) ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
Blur() Blur()
.disabled(true) .disabled(true)
} }
@@ -231,7 +231,7 @@ func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
case .pubkey: case .pubkey:
let pk = m.ref.ref_id let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk).username let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
var attributedString = AttributedString(stringLiteral: "@\(disp)") var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))") attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = DamusColors.purple
@@ -261,13 +261,24 @@ struct NoteArtifacts: Equatable {
} }
let content: CompatibleText let content: CompatibleText
let images: [URL] let urls: [UrlType]
let invoices: [Invoice] let invoices: [Invoice]
let links: [URL]
var media: [MediaUrl] {
return urls.compactMap { url in url.is_media }
}
var images: [URL] {
return urls.compactMap { url in url.is_img }
}
var links: [URL] {
return urls.compactMap { url in url.is_link }
}
static func just_content(_ content: String) -> NoteArtifacts { static func just_content(_ content: String) -> NoteArtifacts {
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content)) let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
return NoteArtifacts(content: txt, images: [], invoices: [], links: []) return NoteArtifacts(content: txt, urls: [], invoices: [])
} }
} }
@@ -304,8 +315,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts { func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
var invoices: [Invoice] = [] var invoices: [Invoice] = []
var img_urls: [URL] = [] var urls: [UrlType] = []
var link_urls: [URL] = []
let one_note_ref = blocks let one_note_ref = blocks
.filter({ $0.is_note_mention }) .filter({ $0.is_note_mention })
@@ -323,12 +333,14 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
return str + mention_str(m, profiles: profiles) return str + mention_str(m, profiles: profiles)
case .text(let txt): case .text(let txt):
var trimmed = txt var trimmed = txt
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) { if let prev = blocks[safe: ind-1],
case .url(let u) = prev,
classify_url(u).is_media != nil {
trimmed = " " + trim_prefix(trimmed) trimmed = " " + trim_prefix(trimmed)
} }
if let next = blocks[safe: ind+1] { if let next = blocks[safe: ind+1] {
if case .url(let u) = next, is_image_url(u) { if case .url(let u) = next, classify_url(u).is_media != nil {
trimmed = trim_suffix(trimmed) trimmed = trim_suffix(trimmed)
} else if case .mention(let m) = next, m.type == .event, one_note_ref { } else if case .mention(let m) = next, m.type == .event, one_note_ref {
trimmed = trim_suffix(trimmed) trimmed = trim_suffix(trimmed)
@@ -345,25 +357,112 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
invoices.append(invoice) invoices.append(invoice)
return str return str
case .url(let url): case .url(let url):
// Handle Image URLs let url_type = classify_url(url)
if is_image_url(url) { switch url_type {
// Append Image case .media:
img_urls.append(url) urls.append(url_type)
return str return str
} else { case .link(let url):
link_urls.append(url) urls.append(url_type)
return str + url_str(url) return str + url_str(url)
} }
} }
} }
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls) return NoteArtifacts(content: txt, urls: urls, invoices: invoices)
} }
func is_image_url(_ url: URL) -> Bool { enum MediaUrl {
case image(URL)
case video(URL)
var url: URL {
switch self {
case .image(let url):
return url
case .video(let url):
return url
}
}
}
enum UrlType {
case media(MediaUrl)
case link(URL)
var url: URL {
switch self {
case .media(let media_url):
switch media_url {
case .image(let url):
return url
case .video(let url):
return url
}
case .link(let url):
return url
}
}
var is_video: URL? {
switch self {
case .media(let media_url):
switch media_url {
case .image:
return nil
case .video(let url):
return url
}
case .link:
return nil
}
}
var is_img: URL? {
switch self {
case .media(let media_url):
switch media_url {
case .image(let url):
return url
case .video:
return nil
}
case .link:
return nil
}
}
var is_link: URL? {
switch self {
case .media:
return nil
case .link(let url):
return url
}
}
var is_media: MediaUrl? {
switch self {
case .media(let murl):
return murl
case .link:
return nil
}
}
}
func classify_url(_ url: URL) -> UrlType {
let str = url.lastPathComponent.lowercased() let str = url.lastPathComponent.lowercased()
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp")
return isUrl if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
return .media(.image(url))
}
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") {
return .media(.video(url))
}
return .link(url)
} }
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? { func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
+41 -25
View File
@@ -14,6 +14,15 @@ enum EventGroupType {
case zap(ZapGroup) case zap(ZapGroup)
case profile_zap(ZapGroup) case profile_zap(ZapGroup)
var is_note_zap: Bool {
switch self {
case .repost: return false
case .reaction: return false
case .zap: return true
case .profile_zap: return false
}
}
var zap_group: ZapGroup? { var zap_group: ZapGroup? {
switch self { switch self {
case .profile_zap(let grp): case .profile_zap(let grp):
@@ -42,18 +51,23 @@ enum EventGroupType {
} }
enum ReactingTo { enum ReactingTo {
case your_post case your_note
case tagged_in case tagged_in
case your_profile case your_profile
} }
func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo { func determine_reacting_to(our_pubkey: String, ev: NostrEvent?, group: EventGroupType, nozaps: Bool) -> ReactingTo {
guard let ev else { guard let ev else {
return .your_profile return .your_profile
} }
if nozaps && group.is_note_zap {
// ZAPPING NOTES IS NOT ALLOWED!!!! EVIL!!!
return .your_profile
}
if ev.pubkey == our_pubkey { if ev.pubkey == our_pubkey {
return .your_post return .your_note
} }
return .tagged_in return .tagged_in
@@ -61,7 +75,7 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
func event_author_name(profiles: Profiles, pubkey: String) -> String { func event_author_name(profiles: Profiles, pubkey: String) -> String {
let alice_prof = profiles.lookup(id: pubkey) let alice_prof = profiles.lookup(id: pubkey)
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50)
} }
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String { func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
@@ -72,7 +86,7 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.") return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
} }
return event_author_name(profiles: profiles, pubkey: zap.request.pubkey) return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
} else { } else {
let ev = group.events[ind] let ev = group.events[ind]
return event_author_name(profiles: profiles, pubkey: ev.pubkey) return event_author_name(profiles: profiles, pubkey: ev.pubkey)
@@ -89,9 +103,9 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
"reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in "reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in "reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in "reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
"reacted_your_post_1" - returned when 1 reaction occurred to the current user's post "reacted_your_note_1" - returned when 1 reaction occurred to the current user's post
"reacted_your_post_2" - returned when 2 reactions occurred to the current user's post "reacted_your_note_2" - returned when 2 reactions occurred to the current user's post
"reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post "reacted_your_note_3" - returned when 3 or more reactions occurred to the current user's post
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile "reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile "reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile "reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
@@ -99,9 +113,9 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
"reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in "reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in "reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in "reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
"reposted_your_post_1" - returned when 1 repost occurred to the current user's post "reposted_your_note_1" - returned when 1 repost occurred to the current user's post
"reposted_your_post_2" - returned when 2 reposts occurred to the current user's post "reposted_your_note_2" - returned when 2 reposts occurred to the current user's post
"reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post "reposted_your_note_3" - returned when 3 or more reposts occurred to the current user's post
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile "reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile "reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile "reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
@@ -109,20 +123,20 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
"zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in "zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in "zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in "zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
"zapped_your_post_1" - returned when 1 zap occurred to the current user's post "zapped_your_note_1" - returned when 1 zap occurred to the current user's post
"zapped_your_post_2" - returned when 2 zaps occurred to the current user's post "zapped_your_note_2" - returned when 2 zaps occurred to the current user's post
"zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post "zapped_your_note_3" - returned when 3 or more zaps occurred to the current user's post
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile "zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile "zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile "zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
*/ */
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String { func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, nozaps: Bool, locale: Locale? = nil) -> String {
if group.events.count == 0 { if group.events.count == 0 {
return "??" return "??"
} }
let verb = reacting_to_verb(group: group) let verb = reacting_to_verb(group: group)
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev) let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev, group: group, nozaps: nozaps)
let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))" let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))"
let format = localizedStringFormat(key: localization_key, locale: locale) let format = localizedStringFormat(key: localization_key, locale: locale)
@@ -162,7 +176,7 @@ struct EventGroupView: View {
let group: EventGroupType let group: EventGroupType
var GroupDescription: some View { var GroupDescription: some View {
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))") Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, nozaps: state.settings.nozaps))")
} }
func ZapIcon(_ zapgrp: ZapGroup) -> some View { func ZapIcon(_ zapgrp: ZapGroup) -> some View {
@@ -207,16 +221,18 @@ struct EventGroupView: View {
if let event { if let event {
let thread = ThreadModel(event: event, damus_state: state) let thread = ThreadModel(event: event, damus_state: state)
let dest = ThreadView(state: state, thread: thread) let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest) { GroupDescription
VStack(alignment: .leading) { if !state.settings.nozaps || !group.is_note_zap {
GroupDescription NavigationLink(destination: dest) {
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content]) VStack(alignment: .leading) {
.padding([.top], 1) EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])
.padding([.trailing]) .padding([.top], 1)
.foregroundColor(.gray) .padding([.trailing])
.foregroundColor(.gray)
}
} }
.buttonStyle(.plain)
} }
.buttonStyle(.plain)
} else { } else {
GroupDescription GroupDescription
} }
@@ -92,7 +92,7 @@ struct NotificationsView: View {
var mystery: some View { var mystery: some View {
VStack(spacing: 20) { VStack(spacing: 20) {
Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name)", comment: "Text telling the user to wake up, where the argument is their display name.") Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
} }
.id("what") .id("what")
+1 -15
View File
@@ -51,21 +51,7 @@ struct ParticipantsView: View {
ForEach(originalReferences.pRefs) { participant in ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id let pubkey = participant.id
HStack { HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) UserView(damus_state: damus_state, pubkey: pubkey)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
if let about = profile?.about {
let blocks = parse_mentions(content: about, tags: [])
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
Text(about_string)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image("check-circle.fill") Image("check-circle.fill")
.font(.system(size: 30)) .font(.system(size: 30))
+39 -35
View File
@@ -13,7 +13,7 @@ enum NostrPostResult {
case cancel case cancel
} }
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.") let POST_PLACEHOLDER = NSLocalizedString("Type your note here...", comment: "Text box prompt to ask user to type their note.")
class TagModel: ObservableObject { class TagModel: ObservableObject {
var diff = 0 var diff = 0
@@ -83,7 +83,9 @@ struct PostView: View {
} }
} }
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) var content = self.post.string
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
.replacingOccurrences(of: "\u{200B}", with: "") // these characters are added when adding mentions.
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ") let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
@@ -109,6 +111,14 @@ struct PostView: View {
var is_post_empty: Bool { var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
} }
var uploading_disabled: Bool {
return image_upload.progress != nil
}
var posting_disabled: Bool {
return is_post_empty || uploading_disabled
}
var ImageButton: some View { var ImageButton: some View {
Button(action: { Button(action: {
@@ -133,7 +143,7 @@ struct PostView: View {
ImageButton ImageButton
CameraButton CameraButton
} }
.disabled(image_upload.progress != nil) .disabled(uploading_disabled)
} }
var PostButton: some View { var PostButton: some View {
@@ -144,12 +154,12 @@ struct PostView: View {
self.send_post() self.send_post()
} }
} }
.disabled(is_post_empty) .disabled(posting_disabled)
.font(.system(size: 14, weight: .bold)) .font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30) .frame(width: 80, height: 30)
.foregroundColor(.white) .foregroundColor(.white)
.background(LINEAR_GRADIENT) .background(LINEAR_GRADIENT)
.opacity(is_post_empty ? 0.5 : 1.0) .opacity(posting_disabled ? 0.5 : 1.0)
.clipShape(Capsule()) .clipShape(Capsule())
} }
@@ -207,29 +217,25 @@ struct PostView: View {
} }
} }
func TextEntry(scrollViewGeometry: GeometryProxy) -> some View { var TextEntry: some View {
GeometryReader { (geometry: GeometryProxy) in ZStack(alignment: .topLeading) {
ZStack(alignment: .topLeading) { TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in focusWordAttributes = (word, range)
focusWordAttributes = (word, range) self.newCursorIndex = nil
self.newCursorIndex = nil })
})
.environmentObject(tagModel) .environmentObject(tagModel)
.frame(maxHeight: scrollViewGeometry.size.height)
.position(x: geometry.frame(in: .local).midX, y: scrollViewGeometry.frame(in: .local).midY)
.focused($focus) .focused($focus)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.onChange(of: post) { p in .onChange(of: post) { p in
post_changed(post: p, media: uploadedMedias) post_changed(post: p, media: uploadedMedias)
} }
if post.string.isEmpty { if post.string.isEmpty {
Text(POST_PLACEHOLDER) Text(POST_PLACEHOLDER)
.padding(.top, 8) .padding(.top, 8)
.padding(.leading, 4) .padding(.leading, 4)
.foregroundColor(Color(uiColor: .placeholderText)) .foregroundColor(Color(uiColor: .placeholderText))
.allowsHitTesting(false) .allowsHitTesting(false)
}
} }
} }
} }
@@ -301,12 +307,12 @@ struct PostView: View {
} }
} }
func Editor(deviceSize: GeometryProxy, scrollViewGeometry: GeometryProxy) -> some View { func Editor(deviceSize: GeometryProxy) -> some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) { HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
TextEntry(scrollViewGeometry: scrollViewGeometry) TextEntry
} }
.frame(height: deviceSize.size.height * multiply_factor) .frame(height: deviceSize.size.height * multiply_factor)
.id("post") .id("post")
@@ -331,18 +337,16 @@ struct PostView: View {
TopBar TopBar
ScrollViewReader { scroller in ScrollViewReader { scroller in
GeometryReader { (geometry: GeometryProxy) in ScrollView {
ScrollView { if case .replying_to(let replying_to) = self.action {
if case .replying_to(let replying_to) = self.action { ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
}
Editor(deviceSize: deviceSize, scrollViewGeometry: geometry)
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
} }
Editor(deviceSize: deviceSize)
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
} }
} }
+2 -2
View File
@@ -58,12 +58,12 @@ struct UserSearch: View {
} }
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString { private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username let name = Profile.displayName(profile: user.profile, pubkey: pk).username.truncate(maxLength: 50)
let tagString = "@\(name)\u{200B} " let tagString = "@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString, let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"]) NSAttributedString.Key.link: "nostr:\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2)) tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2)) tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
+53
View File
@@ -0,0 +1,53 @@
//
// AboutView.swift
// damus
//
// Created by William Casarin on 2023-06-18.
//
import SwiftUI
struct AboutView: View {
let state: DamusState
let about: String
let max_about_length = 280
@State var show_full_about: Bool = false
@State private var about_string: AttributedString? = nil
var body: some View {
Group {
if let about_string {
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline)
if truncated_about != nil {
if show_full_about {
Button(NSLocalizedString("Show less", comment: "Button to show less of a long profile description.")) {
show_full_about = false
}
.font(.footnote)
} else {
Button(NSLocalizedString("Show more", comment: "Button to show more of a long profile description.")) {
show_full_about = true
}
.font(.footnote)
}
}
} else {
Text(verbatim: "")
.font(.subheadline)
}
}
.onAppear {
let blocks = parse_mentions(content: about, tags: [])
about_string = render_blocks(blocks: blocks, profiles: state.profiles).content.attributed
}
}
}
/*
#Preview {
AboutView()
}
*/
@@ -0,0 +1,38 @@
//
// CondensedProfilePicturesView.swift
// damus
//
// Created by Terry Yiu on 6/19/23.
//
import SwiftUI
struct CondensedProfilePicturesView: View {
let state: DamusState
let pubkeys: [String]
let maxPictures: Int
init(state: DamusState, pubkeys: [String], maxPictures: Int) {
self.state = state
self.pubkeys = pubkeys
self.maxPictures = min(maxPictures, pubkeys.count)
}
var body: some View {
// Using ZStack to make profile pictures floating and stacked on top of each other.
ZStack {
ForEach((0..<maxPictures).reversed(), id: \.self) { index in
ProfilePicView(pubkey: pubkeys[index], size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
.offset(x: CGFloat(index) * 20)
}
}
// Padding is needed so that other components drawn adjacent to this view don't get drawn on top.
.padding(.trailing, CGFloat((maxPictures - 1) * 20))
}
}
struct CondensedProfilePicturesView_Previews: PreviewProvider {
static var previews: some View {
CondensedProfilePicturesView(state: test_damus_state(), pubkeys: ["a", "b", "c", "d"], maxPictures: 3)
}
}
+4 -2
View File
@@ -103,13 +103,15 @@ struct EditMetadataView: View {
TopSection TopSection
Form { Form {
Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) {
TextField("Satoshi Nakamoto", text: $display_name) let display_name_placeholder = "Satoshi Nakamoto"
TextField(display_name_placeholder, text: $display_name)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
} }
Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) { Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) {
TextField("satoshi", text: $name) let username_placeholder = "satoshi"
TextField(username_placeholder, text: $name)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
+1 -1
View File
@@ -65,7 +65,7 @@ struct ProfileName: View {
} }
var name_choice: String { var name_choice: String {
return prefix == "@" ? current_display_name.username : current_display_name.display_name return prefix == "@" ? current_display_name.username.truncate(maxLength: 50) : current_display_name.display_name.truncate(maxLength: 50)
} }
var onlyzapper: Bool { var onlyzapper: Bool {
+1
View File
@@ -92,6 +92,7 @@ struct InnerProfilePicView: View {
var Placeholder: some View { var Placeholder: some View {
Circle() Circle()
.frame(width: size, height: size) .frame(width: size, height: size)
.foregroundColor(DamusColors.mediumGrey)
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.padding(2) .padding(2)
} }
+48 -19
View File
@@ -46,6 +46,31 @@ func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String
return String(format: format, locale: locale, count) return String(format: format, locale: locale, count)
} }
func followedByString(_ friend_intersection: [String], profiles: Profiles, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
let names: [String] = friend_intersection.prefix(3).map {
let profile = profiles.lookup(id: $0)
return Profile.displayName(profile: profile, pubkey: $0).username.truncate(maxLength: 20)
}
switch friend_intersection.count {
case 0:
return ""
case 1:
let format = NSLocalizedString("Followed by %@", bundle: bundle, comment: "Text to indicate that the user is followed by one of our follows.")
return String(format: format, locale: locale, names[0])
case 2:
let format = NSLocalizedString("Followed by %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by two of our follows.")
return String(format: format, locale: locale, names[0], names[1])
case 3:
let format = NSLocalizedString("Followed by %@, %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by three of our follows.")
return String(format: format, locale: locale, names[0], names[1], names[2])
default:
let format = localizedStringFormat(key: "followed_by_three_and_others", locale: locale)
return String(format: format, locale: locale, friend_intersection.count - 3, names[0], names[1], names[2])
}
}
struct EditButton: View { struct EditButton: View {
let damus_state: DamusState let damus_state: DamusState
@@ -96,7 +121,6 @@ struct ProfileView: View {
static let markdown = Markdown() static let markdown = Markdown()
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false @State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false @State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false @State var show_qr_code: Bool = false
@@ -106,6 +130,7 @@ struct ProfileView: View {
@StateObject var profile: ProfileModel @StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel @StateObject var followers: FollowersModel
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) { init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state self.damus_state = damus_state
@@ -244,11 +269,7 @@ struct ProfileView: View {
func lnButton(lnurl: String, profile: Profile) -> some View { func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap" let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: { return Button(action: {
if damus_state.settings.show_wallet_selector { present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl))
showing_select_wallet = true
} else {
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
}
}) { }) {
Image(button_img) Image(button_img)
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary) .foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
@@ -275,9 +296,6 @@ struct ProfileView: View {
} }
.cornerRadius(24) .cornerRadius(24)
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
}
} }
var dmButton: some View { var dmButton: some View {
@@ -375,12 +393,7 @@ struct ProfileView: View {
nameSection(profile_data: profile_data) nameSection(profile_data: profile_data)
if let about = profile_data?.about { if let about = profile_data?.about {
let blocks = parse_mentions(content: about, tags: []) AboutView(state: damus_state, about: about)
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
SelectableText(attributedString: about_string, size: .subheadline)
} else {
Text(verbatim: "")
.font(.subheadline)
} }
if let url = profile_data?.website_url { if let url = profile_data?.website_url {
@@ -391,7 +404,7 @@ struct ProfileView: View {
if let contact = profile.contacts { if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id } let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model)) {
HStack { HStack {
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray) let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
@@ -399,7 +412,7 @@ struct ProfileView: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) let fview = FollowersView(damus_state: damus_state)
.environmentObject(followers) .environmentObject(followers)
if followers.contacts != nil { if followers.contacts != nil {
NavigationLink(destination: fview) { NavigationLink(destination: fview) {
@@ -432,6 +445,22 @@ struct ProfileView: View {
} }
} }
} }
if profile.pubkey != damus_state.pubkey {
let friended_followers = damus_state.contacts.get_friended_followers(profile.pubkey)
if !friended_followers.isEmpty {
Spacer()
NavigationLink(destination: FollowersYouKnowView(damus_state: damus_state, friended_followers: friended_followers)) {
HStack {
CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3)
Text(followedByString(friended_followers, profiles: damus_state.profiles))
.font(.subheadline).foregroundColor(.gray)
.multilineTextAlignment(.leading)
}
}
}
}
} }
.padding(.horizontal) .padding(.horizontal)
} }
@@ -447,8 +476,8 @@ struct ProfileView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: { CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only your posts (instead of posts and replies).").tag(FilterState.posts) Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing your posts and replies (instead of only your posts).").tag(FilterState.posts_and_replies) Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
}) })
Divider() Divider()
.frame(height: 1) .frame(height: 1)
+260 -80
View File
@@ -8,11 +8,56 @@
import SwiftUI import SwiftUI
import CoreImage.CIFilterBuiltins import CoreImage.CIFilterBuiltins
struct ProfileScanResult: Equatable {
let pubkey: String
init(hex: String) {
self.pubkey = hex
}
init?(string: String) {
var str = string
guard str.count != 0 else {
return nil
}
if str.hasPrefix("nostr:") {
str.removeFirst("nostr:".count)
}
if let _ = hex_decode(str), str.count == 64 {
self = .init(hex: str)
return
}
if str.starts(with: "npub"), let b32 = try? bech32_decode(str) {
let hex = hex_encode(b32.data)
self = .init(hex: hex)
return
}
return nil
}
}
struct QRCodeView: View { struct QRCodeView: View {
let damus_state: DamusState let damus_state: DamusState
@State var pubkey: String @State var pubkey: String
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@State private var selectedTab = 0
@State var scanResult: ProfileScanResult? = nil
@State var showProfileView: Bool = false
@State var profile: Profile? = nil
@State var error: String? = nil
@State private var outerTrimEnd: CGFloat = 0
var animationDuration: Double = 0.5
let generator = UIImpactFeedbackGenerator(style: .light)
var maybe_key: String? { var maybe_key: String? {
guard let key = bech32_pubkey(pubkey) else { guard let key = bech32_pubkey(pubkey) else {
@@ -22,87 +67,221 @@ struct QRCodeView: View {
return key return key
} }
var body: some View { @ViewBuilder
ZStack(alignment: .center) { func navImage(systemImage: String) -> some View {
Image(systemName: systemImage)
ZStack(alignment: .topLeading) { .frame(width: 33, height: 33)
DamusGradient() .background(Color.black.opacity(0.6))
Button { .clipShape(Circle())
presentationMode.wrappedValue.dismiss() }
} label: {
Image("close") var navBackButton: some View {
.foregroundColor(.white) Button {
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
}
VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: pubkey)
if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.foregroundColor(DamusColors.white)
.padding(.top, 50)
}
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(DamusColors.white)
.font(.body)
}
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
if (pubkey == damus_state.pubkey) {
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
} else {
Text("Follow them on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user (someone else).")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
}
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.foregroundColor(DamusColors.white)
.font(.system(size: 18, weight: .ultraLight))
Spacer()
}
}
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
})) } label: {
navImage(systemImage: "chevron.left")
}
}
var customNavbar: some View {
HStack {
navBackButton
Spacer()
}
.padding(.top, 5)
.padding(.horizontal)
.accentColor(DamusColors.white)
}
var body: some View {
NavigationView {
ZStack(alignment: .center) {
ZStack(alignment: .topLeading) {
DamusGradient()
}
TabView(selection: $selectedTab) {
QRView
.tag(0)
if pubkey == damus_state.pubkey {
QRCameraView()
.tag(1)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.onAppear {
UIScrollView.appearance().isScrollEnabled = false
}
.gesture(
DragGesture()
.onChanged { _ in }
)
}
}
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
}
var QRView: some View {
VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.padding(.top, 50)
}
if let display_name = profile?.display_name {
Text(display_name)
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.font(.body)
}
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.font(.system(size: 18, weight: .ultraLight))
Spacer()
Button(action: {
selectedTab = 1
}) {
HStack {
Text("Scan Code", comment: "Button to switch to scan QR Code page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(50)
}
}
func QRCameraView() -> some View {
return VStack(alignment: .center) {
Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.")
.padding(.top, 50)
.font(.system(size: 24, weight: .heavy))
Spacer()
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in
switch result {
case .success(let success):
handleProfileScan(success.string)
case .failure(let failure):
self.error = failure.localizedDescription
}
}
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
.rotationEffect(.degrees(-90)))
.shadow(radius: 10)
Spacer()
if let scanResult {
let dst = ProfileView(damus_state: damus_state, pubkey: scanResult.pubkey)
NavigationLink(destination: dst, isActive: $showProfileView) {
EmptyView()
}
}
Spacer()
Button(action: {
selectedTab = 0
}) {
HStack {
Text("View QR Code", comment: "Button to switch to view users QR Code")
.fontWeight(.semibold)
}
.frame( maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(50)
}
}
func handleProfileScan(_ scanned_str: String) {
guard let result = ProfileScanResult(string: scanned_str) else {
self.error = "Invalid profile QR"
return
}
self.error = nil
guard result != self.scanResult else {
return
}
generator.impactOccurred()
cameraAnimate {
scanResult = result
find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in
guard let res else {
error = "Profile not found"
return
}
switch res {
case .invalid_profile:
error = "Profile was found but was corrupt."
case .profile:
show_profile_after_delay()
case .event:
print("invalid search result")
}
}
}
}
func show_profile_after_delay() {
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
showProfileView = true
}
}
func cameraAnimate(completion: @escaping () -> Void) {
outerTrimEnd = 0.0
withAnimation(.easeInOut(duration: animationDuration)) {
outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done.
}
completion()
} }
func generateQRCode(pubkey: String) -> UIImage { func generateQRCode(pubkey: String) -> UIImage {
@@ -130,3 +309,4 @@ struct QRCodeView_Previews: PreviewProvider {
QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey) QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
} }
} }
+12 -6
View File
@@ -75,11 +75,13 @@ struct RelayDetailView: View {
UserViewRow(damus_state: state, pubkey: pubkey) UserViewRow(damus_state: state, pubkey: pubkey)
} }
} }
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) { if let relay_connection {
HStack { Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
Text(relay) HStack {
Spacer() Text(relay)
RelayStatus(pool: state.pool, relay: relay) Spacer()
RelayStatusView(connection: relay_connection)
}
} }
} }
if nip11.is_paid { if nip11.is_paid {
@@ -88,7 +90,7 @@ struct RelayDetailView: View {
}, header: { }, header: {
Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.") Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.")
}, footer: { }, footer: {
Text("This is a paid relay, you must pay for posts to be accepted.", comment: "Footer description that explains that the relay server requires payment to post.") 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.")
}) })
} }
@@ -134,6 +136,10 @@ struct RelayDetailView: View {
} }
return attrString return attrString
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
}
} }
struct RelayDetailView_Previews: PreviewProvider { struct RelayDetailView_Previews: PreviewProvider {
-65
View File
@@ -1,65 +0,0 @@
//
// RelayStatus.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayStatus: View {
let pool: RelayPool
let relay: String
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
@State var conn_color: Color = .gray
@State var conn_image: String = "network"
@State var connecting: Bool = false
func update_connection() {
for relay in pool.relays {
if relay.id == self.relay {
let c = relay.connection
if c.isConnected {
conn_image = "globe"
conn_color = .green
} else if c.isConnecting {
connecting = true
} else {
conn_image = "warning.fill"
conn_color = .red
}
}
}
}
var body: some View {
HStack {
if connecting {
ProgressView()
.frame(width: 20, height: 20)
.padding(.trailing, 5)
} else {
Image(conn_image)
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(conn_color)
.padding(.trailing, 5)
}
}
.onReceive(timer) { _ in
update_connection()
}
.onAppear() {
update_connection()
}
}
}
struct RelayStatus_Previews: PreviewProvider {
static var previews: some View {
RelayStatus(pool: test_damus_state().pool, relay: "relay")
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// RelayStatusView.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayStatusView: View {
@ObservedObject var connection: RelayConnection
var body: some View {
Group {
if connection.isConnecting {
ProgressView()
} else {
Image(connection.isConnected ? "globe" : "warning.fill")
.resizable()
.foregroundColor(connection.isConnected ? .green : .red)
}
}
.frame(width: 20, height: 20)
.padding(.trailing, 5)
}
}
struct RelayStatusView_Previews: PreviewProvider {
static var previews: some View {
let connection = test_damus_state().pool.get_relay("relay")!.connection
RelayStatusView(connection: connection)
}
}
+7 -1
View File
@@ -26,12 +26,18 @@ struct RelayToggle: View {
var body: some View { var body: some View {
HStack { HStack {
RelayStatus(pool: state.pool, relay: relay_id) if let relay_connection {
RelayStatusView(connection: relay_connection)
}
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false) RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false)
Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id)) Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id))
.toggleStyle(SwitchToggleStyle(tint: .accentColor)) .toggleStyle(SwitchToggleStyle(tint: .accentColor))
} }
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay_id)?.connection
}
} }
struct RelayToggle_Previews: PreviewProvider { struct RelayToggle_Previews: PreviewProvider {
+6 -2
View File
@@ -20,8 +20,8 @@ struct RelayView: View {
if showActionButtons { if showActionButtons {
RemoveButton(privkey: privkey, showText: false) RemoveButton(privkey: privkey, showText: false)
} }
else { else if let relay_connection {
RelayStatus(pool: state.pool, relay: relay) RelayStatusView(connection: relay_connection)
} }
} }
@@ -67,6 +67,10 @@ struct RelayView: View {
} }
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
}
func CopyAction(relay: String) -> some View { func CopyAction(relay: String) -> some View {
Button { Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text") UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
+1 -1
View File
@@ -22,7 +22,7 @@ struct ReplyView: View {
.map { pubkey in .map { pubkey in
let pk = pubkey.ref_id let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk) let prof = damus.profiles.lookup(id: pk)
return "@" + Profile.displayName(profile: prof, pubkey: pk).username return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
} }
.joined(separator: " ") .joined(separator: " ")
if names.isEmpty { if names.isEmpty {
+8 -12
View File
@@ -72,24 +72,20 @@ struct SearchingEventView: View {
} }
case .event: case .event:
if let ev = state.events.lookup(evid) { find_event(state: state, query: .event(evid: evid)) { res in
self.search_state = .found(ev) guard case .event(let ev) = res else {
return
}
find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { ev in
if let ev {
self.search_state = .found(ev)
} else {
self.search_state = .not_found self.search_state = .not_found
return
} }
self.search_state = .found(ev)
} }
case .profile: case .profile:
find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { ev in find_event(state: state, query: .profile(pubkey: evid)) { res in
if state.profiles.lookup(id: evid) != nil { guard case .profile(_, let ev) = res else {
self.search_state = .found_profile(evid)
} else {
self.search_state = .not_found self.search_state = .not_found
return
} }
self.search_state = .found_profile(ev.pubkey)
} }
} }
} }
+4 -4
View File
@@ -9,7 +9,7 @@ import SwiftUI
struct SelectWalletView: View { struct SelectWalletView: View {
let default_wallet: Wallet let default_wallet: Wallet
@Binding var showingSelectWallet: Bool @Binding var active_sheet: Sheets?
let our_pubkey: String let our_pubkey: String
let invoice: String let invoice: String
@State var invoice_copied: Bool = false @State var invoice_copied: Bool = false
@@ -59,7 +59,7 @@ struct SelectWalletView: View {
}.padding(.vertical, 2.5) }.padding(.vertical, 2.5)
} }
}.navigationBarTitle(Text("Pay the Lightning invoice", comment: "Navigation bar title for view to pay Lightning invoice."), displayMode: .inline).navigationBarItems(trailing: Button(action: { }.navigationBarTitle(Text("Pay the Lightning invoice", comment: "Navigation bar title for view to pay Lightning invoice."), displayMode: .inline).navigationBarItems(trailing: Button(action: {
self.showingSelectWallet = false self.active_sheet = nil
}) { }) {
Text("Done", comment: "Button to dismiss wallet selection view for paying Lightning invoice.").bold() Text("Done", comment: "Button to dismiss wallet selection view for paying Lightning invoice.").bold()
}) })
@@ -68,9 +68,9 @@ struct SelectWalletView: View {
} }
struct SelectWalletView_Previews: PreviewProvider { struct SelectWalletView_Previews: PreviewProvider {
@State static var show: Bool = true @State static var active_sheet: Sheets? = nil
static var previews: some View { static var previews: some View {
SelectWalletView(default_wallet: .lnlink, showingSelectWallet: $show, our_pubkey: "", invoice: "") SelectWalletView(default_wallet: .lnlink, active_sheet: $active_sheet, our_pubkey: "", invoice: "")
} }
} }
+13 -18
View File
@@ -34,18 +34,13 @@ struct SetupView: View {
.shadow(color: DamusColors.purple, radius: 2) .shadow(color: DamusColors.purple, radius: 2)
.frame(width: 56, height: 56, alignment: .center) .frame(width: 56, height: 56, alignment: .center)
.padding(.top, 20.0) .padding(.top, 20.0)
HStack { Text("Welcome to Damus", comment: "Welcome text shown on the first screen when user is not logged in.")
Text("Welcome to", comment: "Welcome text shown on the first screen when user is not logged in.") .font(.title)
.font(.title) .fontWeight(.heavy)
.fontWeight(.heavy) .foregroundStyle(DamusLogoGradient.gradient)
Text("Damus")
.font(.title) Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is")
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
}
Text("The go-to iOS nostr client", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
.padding(.top, 10) .padding(.top, 10)
@@ -61,7 +56,7 @@ struct SetupView: View {
eula.toggle() eula.toggle()
}) { }) {
HStack { HStack {
Text("Let's get started!", comment: "Button to continue to login page.") Text("Let's get started!", comment: "Button to continue to login page.")
.fontWeight(.semibold) .fontWeight(.semibold)
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
@@ -90,7 +85,7 @@ struct LearnAboutNostrLink: View {
Button(action: { Button(action: {
openURL(URL(string: "https://nostr.com")!) openURL(URL(string: "https://nostr.com")!)
}, label: { }, label: {
Text("Learn more about nostr") Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
}) })
@@ -106,11 +101,11 @@ struct WhatIsNostr: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Image("nostr-logo") Image("nostr-logo")
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("What is nostr?") Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.")
.fontWeight(.bold) .fontWeight(.bold)
.padding(.vertical, 10) .padding(.vertical, 10)
Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network") Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
LearnAboutNostrLink() LearnAboutNostrLink()
@@ -125,11 +120,11 @@ struct WhyWeNeedNostr: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Image("lightbulb") Image("lightbulb")
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Why we need nostr?") Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.")
.fontWeight(.bold) .fontWeight(.bold)
.padding(.vertical, 10) .padding(.vertical, 10)
Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken") Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
} }
} }
+12 -2
View File
@@ -10,7 +10,7 @@ import SwiftUI
struct ThreadView: View { struct ThreadView: View {
let state: DamusState let state: DamusState
@StateObject var thread: ThreadModel @ObservedObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var parent_events: [NostrEvent] { var parent_events: [NostrEvent] {
@@ -22,11 +22,13 @@ struct ThreadView: View {
} }
var body: some View { var body: some View {
//let top_zap = get_top_zap(events: state.events, evid: thread.event.id)
ScrollViewReader { reader in ScrollViewReader { reader in
ScrollView { ScrollView {
LazyVStack { LazyVStack {
// MARK: - Parents events view // MARK: - Parents events view
ForEach(parent_events, id: \.id) { parent_event in ForEach(parent_events, id: \.id) { parent_event in
MutedEventView(damus_state: state, MutedEventView(damus_state: state,
event: parent_event, event: parent_event,
selected: false) selected: false)
@@ -39,6 +41,7 @@ struct ThreadView: View {
Divider() Divider()
.padding(.top, 4) .padding(.top, 4)
.padding(.leading, 25 * 2) .padding(.leading, 25 * 2)
}.background(GeometryReader { geometry in }.background(GeometryReader { geometry in
// get the height and width of the EventView view // get the height and width of the EventView view
let eventHeight = geometry.frame(in: .global).height let eventHeight = geometry.frame(in: .global).height
@@ -59,6 +62,13 @@ struct ThreadView: View {
) )
.id(self.thread.event.id) .id(self.thread.event.id)
/*
if let top_zap {
ZapEvent(damus: state, zap: top_zap, is_top_zap: true)
.padding(.horizontal)
}
*/
ForEach(child_events, id: \.id) { child_event in ForEach(child_events, id: \.id) { child_event in
MutedEventView( MutedEventView(
damus_state: state, damus_state: state,
@@ -70,7 +80,7 @@ struct ThreadView: View {
thread.set_active_event(child_event) thread.set_active_event(child_event)
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false) scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
} }
Divider() Divider()
.padding([.top], 4) .padding([.top], 4)
} }
@@ -15,10 +15,14 @@ struct InnerTimelineView: View {
@State var nav_target: NostrEvent @State var nav_target: NostrEvent
@State var navigating: Bool = false @State var navigating: Bool = false
static var count: Int = 0
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool) { init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool) {
self.events = events self.events = events
self.state = damus self.state = damus
self.filter = filter self.filter = filter
print("rendering InnerTimelineView \(InnerTimelineView.count)")
InnerTimelineView.count += 1
// dummy event to avoid MaybeThreadView // dummy event to avoid MaybeThreadView
self._nav_target = State(initialValue: test_event) self._nav_target = State(initialValue: test_event)
} }
+1 -3
View File
@@ -31,9 +31,7 @@ struct TimelineView: View {
.shimmer(loading) .shimmer(loading)
.disabled(loading) .disabled(loading)
.background(GeometryReader { proxy -> Color in .background(GeometryReader { proxy -> Color in
DispatchQueue.main.async { handle_scroll_queue(proxy, queue: self.events)
handle_scroll_queue(proxy, queue: self.events)
}
return Color.clear return Color.clear
}) })
} }
+83
View File
@@ -0,0 +1,83 @@
//
// VideoPlayerView.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
struct DamusVideoPlayer: View {
var url: URL
@ObservedObject var model: VideoPlayerModel
@Binding var video_size: CGSize?
var mute_icon: String {
if model.has_audio == false || model.muted {
return "speaker.slash"
} else {
return "speaker"
}
}
var mute_icon_color: Color {
switch self.model.has_audio {
case .none:
return .white
case .some(let has_audio):
return has_audio ? .white : .red
}
}
var MuteIcon: some View {
ZStack {
Circle()
.opacity(0.2)
.frame(width: 32, height: 32)
.foregroundColor(.black)
Image(systemName: mute_icon)
.padding()
.foregroundColor(mute_icon_color)
}
}
var body: some View {
GeometryReader { geo in
let localFrame = geo.frame(in: .local)
let localCenter = CGPoint(x: localFrame.midX, y: localFrame.midY)
let globalCenter = geo.frame(in: .global).origin.applying(.init(translationX: localCenter.x, y: localCenter.y))
let centerY = globalCenter.y
ZStack(alignment: .bottomTrailing) {
VideoPlayer(url: url, model: model)
if model.has_audio == true {
MuteIcon
.zIndex(11.0)
.onTapGesture {
self.model.muted = !self.model.muted
}
}
}
.onChange(of: model.size) { size in
guard let size else {
return
}
video_size = size
}
.onChange(of: centerY) { _ in
let screenHeight = UIScreen.main.bounds.height
let screenMidY = screenHeight / 2
let tol = 0.20 * screenHeight /// tolerance - can vary to taste ie., % of screen height of a centered box in which video plays
model.play = centerY > screenMidY - tol && centerY < screenMidY + tol /// video plays when inside tolerance box
}
}
}
}
struct DamusVideoPlayer_Previews: PreviewProvider {
@StateObject static var model: VideoPlayerModel = VideoPlayerModel()
static var previews: some View {
DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, model: model, video_size: .constant(nil))
}
}
+348
View File
@@ -0,0 +1,348 @@
//
// VideoPlayer.swift
// damus
//
// Created by William Casarin on 2023-05-25.
//
import Foundation
//
// VideoPlayer.swift
// VideoPlayer
//
// Created by Gesen on 2019/7/7.
// Copyright © 2019 Gesen. All rights reserved.
//
import AVFoundation
import GSPlayer
import SwiftUI
public enum VideoState {
/// From the first load to get the first frame of the video
case loading
/// Playing now
case playing(totalDuration: Double)
/// Pause, will be called repeatedly when the buffer progress changes
case paused(playProgress: Double, bufferProgress: Double)
/// An error occurred and cannot continue playing
case error(NSError)
}
enum VideoHandler {
case onBufferChanged((Double) -> Void)
case onPlayToEndTime(() -> Void)
case onReplay(() -> Void)
case onStateChanged((VideoState) -> Void)
}
@MainActor
public class VideoPlayerModel: ObservableObject {
@Published var autoReplay: Bool = true
@Published var muted: Bool = true
@Published var play: Bool = true
@Published var size: CGSize? = nil
@Published var has_audio: Bool? = nil
@Published var contentMode: UIView.ContentMode = .scaleAspectFill
var time: CMTime = CMTime()
var handlers: [VideoHandler] = []
init() {
}
func stop() {
self.play = false
}
func start() {
self.play = true
}
func mute() {
self.muted = true
}
func unmute() {
self.muted = false
}
/// Whether the video will be automatically replayed until the end of the video playback.
func autoReplay(_ value: Bool) -> Self {
autoReplay = value
return self
}
/// Whether the video is muted, only for this instance.
func mute(_ value: Bool) -> Self {
muted = value
return self
}
/// A string defining how the video is displayed within an AVPlayerLayer bounds rect.
/// scaleAspectFill -> resizeAspectFill, scaleAspectFit -> resizeAspect, other -> resize
func contentMode(_ value: UIView.ContentMode) -> Self {
contentMode = value
return self
}
/// Trigger a callback when the buffer progress changes,
/// the value is between 0 and 1.
func onBufferChanged(_ handler: @escaping (Double) -> Void) -> Self {
self.handlers.append(.onBufferChanged(handler))
return self
}
/// Playing to the end.
func onPlayToEndTime(_ handler: @escaping () -> Void) -> Self {
self.handlers.append(.onPlayToEndTime(handler))
return self
}
/// Replay after playing to the end.
func onReplay(_ handler: @escaping () -> Void) -> Self {
self.handlers.append(.onReplay(handler))
return self
}
/// Playback status changes, such as from play to pause.
func onStateChanged(_ handler: @escaping (VideoState) -> Void) -> Self {
self.handlers.append(.onStateChanged(handler))
return self
}
}
@available(iOS 13, *)
public struct VideoPlayer {
private(set) var url: URL
@ObservedObject var model: VideoPlayerModel
/// Init video player instance.
/// - Parameters:
/// - url: http/https URL
/// - play: play/pause
/// - time: current time
public init(url: URL, model: VideoPlayerModel) {
self.url = url
self._model = ObservedObject(wrappedValue: model)
}
}
@available(iOS 13, *)
public extension VideoPlayer {
/// Set the preload size, the default value is 1024 * 1024, unit is byte.
static var preloadByteCount: Int {
get { VideoPreloadManager.shared.preloadByteCount }
set { VideoPreloadManager.shared.preloadByteCount = newValue }
}
/// Set the video urls to be preload queue.
/// Preloading will automatically cache a short segment of the beginning of the video
/// and decide whether to start or pause the preload based on the buffering of the currently playing video.
/// - Parameter urls: URL array
static func preload(urls: [URL]) {
VideoPreloadManager.shared.set(waiting: urls)
}
/// Set custom http header, such as token.
static func customHTTPHeaderFields(transform: @escaping (URL) -> [String: String]?) {
VideoLoadManager.shared.customHTTPHeaderFields = transform
}
/// Get the total size of the video cache.
static func calculateCachedSize() -> UInt {
return VideoCacheManager.calculateCachedSize()
}
/// Clean up all caches.
static func cleanAllCache() {
try? VideoCacheManager.cleanAllCache()
}
}
func get_video_size(player: AVPlayer) async -> CGSize? {
let res = Task.detached(priority: .background) {
return player.currentImage?.size
}
return await res.value
}
func video_has_audio(player: AVPlayer) async -> Bool {
let tracks = try? await player.currentItem?.asset.load(.tracks)
return tracks?.filter({ t in t.mediaType == .audio }).first != nil
}
@available(iOS 13, *)
extension VideoPlayer: UIViewRepresentable {
public func makeUIView(context: Context) -> VideoPlayerView {
let uiView = VideoPlayerView()
uiView.playToEndTime = {
if self.model.autoReplay == false {
self.model.play = false
}
DispatchQueue.main.async {
for handler in model.handlers {
if case .onPlayToEndTime(let cb) = handler {
cb()
}
}
}
}
uiView.contentMode = self.model.contentMode
uiView.replay = {
DispatchQueue.main.async {
for handler in model.handlers {
if case .onReplay(let cb) = handler {
cb()
}
}
}
}
uiView.stateDidChanged = { [unowned uiView] _ in
let state: VideoState = uiView.convertState()
if case .playing = state {
context.coordinator.startObserver(uiView: uiView)
if let player = uiView.player {
Task {
let has_audio = await video_has_audio(player: player)
let size = await get_video_size(player: player)
Task { @MainActor in
if let size {
self.model.size = size
}
self.model.has_audio = has_audio
}
}
}
} else {
context.coordinator.stopObserver(uiView: uiView)
}
DispatchQueue.main.async {
for handler in model.handlers {
if case .onStateChanged(let cb) = handler {
cb(state)
}
}
}
}
return uiView
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public func updateUIView(_ uiView: VideoPlayerView, context: Context) {
if context.coordinator.observingURL != url {
context.coordinator.clean()
context.coordinator.observingURL = url
}
if model.play {
uiView.play(for: url)
} else {
uiView.pause(reason: .userInteraction)
}
uiView.isMuted = model.muted
uiView.isAutoReplay = model.autoReplay
if let observerTime = context.coordinator.observerTime, model.time != observerTime {
uiView.seek(to: model.time, toleranceBefore: model.time, toleranceAfter: model.time, completion: { _ in })
}
}
public static func dismantleUIView(_ uiView: VideoPlayerView, coordinator: VideoPlayer.Coordinator) {
uiView.pause(reason: .hidden)
}
public class Coordinator: NSObject {
var videoPlayer: VideoPlayer
var observingURL: URL?
var observer: Any?
var observerTime: CMTime?
var observerBuffer: Double?
init(_ videoPlayer: VideoPlayer) {
self.videoPlayer = videoPlayer
}
@MainActor
func startObserver(uiView: VideoPlayerView) {
guard observer == nil else { return }
observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in
guard let `self` = self else { return }
Task { @MainActor in
self.videoPlayer.model.time = time
}
self.observerTime = time
self.updateBuffer(uiView: uiView)
}
}
func stopObserver(uiView: VideoPlayerView) {
guard let observer = observer else { return }
uiView.removeTimeObserver(observer)
self.observer = nil
}
func clean() {
self.observingURL = nil
self.observer = nil
self.observerTime = nil
self.observerBuffer = nil
}
@MainActor
func updateBuffer(uiView: VideoPlayerView) {
let bufferProgress = uiView.bufferProgress
guard bufferProgress != observerBuffer else { return }
for handler in videoPlayer.model.handlers {
if case .onBufferChanged(let cb) = handler {
DispatchQueue.main.async {
cb(bufferProgress)
}
}
}
observerBuffer = bufferProgress
}
}
}
private extension VideoPlayerView {
func convertState() -> VideoState {
switch state {
case .none, .loading:
return .loading
case .playing:
return .playing(totalDuration: totalDuration)
case .paused(let p, let b):
return .paused(playProgress: p, bufferProgress: b)
case .error(let error):
return .error(error)
}
}
}
+49 -10
View File
@@ -45,26 +45,65 @@ enum WalletScanResult: Equatable {
case scanning 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 { struct WalletScannerView: View {
@Binding var result: WalletScanResult @Binding var result: WalletScanResult
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
CodeScannerView(codeTypes: [.qr]) { res in VStack {
switch res { CodeScannerView(codeTypes: [.qr]) { res in
case .success(let success): switch res {
guard let url = WalletConnectURL(str: success.string) else { case .success(let success):
guard let url = WalletConnectURL(str: success.string) else {
result = .failed
return
}
result = .success(url)
case .failure:
result = .failed result = .failed
return
} }
result = .success(url) dismiss()
case .failure:
result = .failed
} }
NWCPaste(result: $result)
dismiss() .padding(.vertical)
} }
} }
} }
+5 -3
View File
@@ -20,9 +20,11 @@ struct WalletView: View {
func MainWalletView(nwc: WalletConnectURL) -> some View { func MainWalletView(nwc: WalletConnectURL) -> some View {
VStack { VStack {
SupportDamus if !damus_state.settings.nozaps {
SupportDamus
Spacer()
Spacer()
}
Text(verbatim: nwc.relay.id) Text(verbatim: nwc.relay.id)
+83 -99
View File
@@ -46,20 +46,12 @@ func satsString(_ count: Int, locale: Locale = Locale.current) -> String {
struct CustomizeZapView: View { struct CustomizeZapView: View {
let state: DamusState let state: DamusState
let event: NostrEvent let target: ZapTarget
let lnurl: String let lnurl: String
@State var comment: String
@State var custom_amount: String
@State var custom_amount_sats: Int?
@State var zap_type: ZapType
@State var invoice: String
@State var error: String?
@State var showing_wallet_selector: Bool
@State var zapping: Bool
@State var show_zap_types: Bool = false
let zap_amounts: [ZapAmountItem] let zap_amounts: [ZapAmountItem]
@StateObject var model: CustomizeZapModel = CustomizeZapModel()
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -71,22 +63,13 @@ struct CustomizeZapView: View {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
init(state: DamusState, event: NostrEvent, lnurl: String) { init(state: DamusState, target: ZapTarget, lnurl: String) {
self._comment = State(initialValue: "") self.target = target
self.event = event
self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount) self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
self._showing_wallet_selector = State(initialValue: false)
self._zap_type = State(initialValue: state.settings.default_zap_type)
self._custom_amount = State(initialValue: String(state.settings.default_zap_amount))
self._custom_amount_sats = State(initialValue: nil)
self._zapping = State(initialValue: false)
self.lnurl = lnurl self.lnurl = lnurl
self.state = state self.state = state
} }
func amount_parts(_ n: Int) -> [ZapAmountItem] { func amount_parts(_ n: Int) -> [ZapAmountItem] {
var i: Int = -1 var i: Int = -1
let start = n * 3 let start = n * 3
@@ -101,7 +84,10 @@ struct CustomizeZapView: View {
func AmountsPart(n: Int) -> some View { func AmountsPart(n: Int) -> some View {
HStack(alignment: .center, spacing: 15) { HStack(alignment: .center, spacing: 15) {
ForEach(amount_parts(n)) { entry in ForEach(amount_parts(n)) { entry in
ZapAmountButton(zapAmountItem: entry, action: {custom_amount_sats = entry.amount; custom_amount = String(entry.amount)}) ZapAmountButton(zapAmountItem: entry, action: {
model.custom_amount_sats = entry.amount
model.custom_amount = String(entry.amount)
})
} }
} }
} }
@@ -125,17 +111,17 @@ struct CustomizeZapView: View {
.font(.headline) .font(.headline)
.frame(width: 70, height: 70) .frame(width: 70, height: 70)
.foregroundColor(fontColor()) .foregroundColor(fontColor())
.background(custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey) .background(model.custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey)
.cornerRadius(15) .cornerRadius(15)
.overlay(RoundedRectangle(cornerRadius: 15) .overlay(RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.purple.opacity(custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2)) .stroke(DamusColors.purple.opacity(model.custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2))
} }
} }
var CustomZapTextField: some View { var CustomZapTextField: some View {
VStack(alignment: .center, spacing: 0) { VStack(alignment: .center, spacing: 0) {
TextField("", text: $custom_amount) TextField("", text: $model.custom_amount)
.placeholder(when: custom_amount.isEmpty, alignment: .center) { .placeholder(when: model.custom_amount.isEmpty, alignment: .center) {
Text(verbatim: 0.formatted()) Text(verbatim: 0.formatted())
} }
.accentColor(.clear) .accentColor(.clear)
@@ -143,16 +129,16 @@ struct CustomizeZapView: View {
.minimumScaleFactor(0.01) .minimumScaleFactor(0.01)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.onReceive(Just(custom_amount)) { newValue in .onChange(of: model.custom_amount) { newValue in
if let parsed = handle_string_amount(new_value: newValue) { if let parsed = handle_string_amount(new_value: newValue) {
self.custom_amount = parsed.formatted() model.custom_amount = parsed.formatted()
self.custom_amount_sats = parsed model.custom_amount_sats = parsed
} else { } else {
self.custom_amount = "" model.custom_amount = ""
self.custom_amount_sats = nil model.custom_amount_sats = nil
} }
} }
Text(verbatim: satsString(custom_amount_sats ?? 0)) Text(verbatim: satsString(model.custom_amount_sats ?? 0))
.font(.system(size: 18, weight: .heavy)) .font(.system(size: 18, weight: .heavy))
} }
} }
@@ -160,12 +146,12 @@ struct CustomizeZapView: View {
var ZapReply: some View { var ZapReply: some View {
HStack { HStack {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment, axis: .vertical) TextField(NSLocalizedString("Send a message with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $model.comment, axis: .vertical)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.lineLimit(5) .lineLimit(5)
} else { } else {
TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment) TextField(NSLocalizedString("Send a message with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $model.comment)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
} }
@@ -179,24 +165,24 @@ struct CustomizeZapView: View {
var ZapButton: some View { var ZapButton: some View {
VStack { VStack {
if zapping { if model.zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.") Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else { } else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) { Button(NSLocalizedString("Zap User", comment: "Button to send a zap.")) {
let amount = custom_amount_sats let amount = model.custom_amount_sats
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type) send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: model.comment, amount_sats: amount, zap_type: model.zap_type)
self.zapping = true model.zapping = true
} }
.disabled(custom_amount_sats == 0 || custom_amount.isEmpty) .disabled(model.custom_amount_sats == 0 || model.custom_amount.isEmpty)
.font(.system(size: 28, weight: .bold)) .font(.system(size: 28, weight: .bold))
.frame(width: 130, height: 50) .frame(width: 180, height: 50)
.foregroundColor(.white) .foregroundColor(.white)
.background(LINEAR_GRADIENT) .background(LINEAR_GRADIENT)
.opacity(custom_amount_sats == 0 || custom_amount.isEmpty ? 0.5 : 1.0) .opacity(model.custom_amount_sats == 0 || model.custom_amount.isEmpty ? 0.5 : 1.0)
.clipShape(Capsule()) .clipShape(Capsule())
} }
if let error { if let error = model.error {
Text(error) Text(error)
.foregroundColor(.red) .foregroundColor(.red)
} }
@@ -208,54 +194,87 @@ struct CustomizeZapView: View {
guard zap_ev.is_custom else { guard zap_ev.is_custom else {
return return
} }
guard zap_ev.event.id == event.id else { guard zap_ev.target.id == target.id else {
return return
} }
self.zapping = false model.zapping = false
switch zap_ev.type { switch zap_ev.type {
case .failed(let err): case .failed(let err):
switch err { switch err {
case .fetching_invoice: case .fetching_invoice:
self.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") model.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
case .bad_lnurl: case .bad_lnurl:
self.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") model.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
case .canceled:
model.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
case .send_failed:
model.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
} }
break break
case .got_zap_invoice(let inv): case .got_zap_invoice(let inv):
if state.settings.show_wallet_selector { if state.settings.show_wallet_selector {
self.invoice = inv model.invoice = inv
self.showing_wallet_selector = true present_sheet(.select_wallet(invoice: inv))
} else { } else {
end_editing() end_editing()
let wallet = state.settings.default_wallet.model let wallet = state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv) open_with_wallet(wallet: wallet, invoice: inv)
self.showing_wallet_selector = false
dismiss() dismiss()
} }
case .sent_from_nwc:
dismiss()
} }
} }
var body: some View { var body: some View {
MainContent VStack(alignment: .center, spacing: 20) {
.sheet(isPresented: $showing_wallet_selector) { ScrollView {
SelectWalletView(default_wallet: state.settings.default_wallet, showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice) HStack(alignment: .center) {
UserView(damus_state: state, pubkey: target.pubkey)
ZapTypeButton()
}
.padding([.horizontal, .top])
CustomZapTextField
AmountPicker
ZapReply
ZapButton
Spacer()
} }
.onReceive(handle_notify(.zapping)) { notif in }
receive_zap(notif: notif) .sheet(isPresented: $model.show_zap_types) {
} if #available(iOS 16.0, *) {
.background(fillColor().edgesIgnoringSafeArea(.all)) ZapPicker
.onTapGesture { .presentationDetents([.medium])
hideKeyboard() .presentationDragIndicator(.visible)
} else {
ZapPicker
} }
}
.onAppear {
model.set_defaults(settings: state.settings)
}
.onReceive(handle_notify(.zapping)) { notif in
receive_zap(notif: notif)
}
.background(fillColor().edgesIgnoringSafeArea(.all))
.onTapGesture {
hideKeyboard()
}
} }
func ZapTypeButton() -> some View { func ZapTypeButton() -> some View {
Button(action: { Button(action: {
show_zap_types = true model.show_zap_types = true
}) { }) {
switch zap_type { switch model.zap_type {
case .pub: case .pub:
Image("globe") Image("globe")
Text("Public", comment: "Button text to indicate that the zap type is a public zap.") Text("Public", comment: "Button text to indicate that the zap type is a public zap.")
@@ -277,43 +296,8 @@ struct CustomizeZapView: View {
.cornerRadius(15) .cornerRadius(15)
} }
var CustomZap: some View {
VStack(alignment: .center, spacing: 20) {
ZapTypeButton()
.padding(.top, 50)
Spacer()
CustomZapTextField
AmountPicker
ZapReply
ZapButton
Spacer()
Spacer()
}
.sheet(isPresented: $show_zap_types) {
if #available(iOS 16.0, *) {
ZapPicker
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
} else {
ZapPicker
}
}
}
var ZapPicker: some View { var ZapPicker: some View {
ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: event.pubkey) ZapTypePicker(zap_type: $model.zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey)
}
var MainContent: some View {
CustomZap
} }
} }
@@ -326,7 +310,7 @@ extension View {
struct CustomizeZapView_Previews: PreviewProvider { struct CustomizeZapView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "") CustomizeZapView(state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "")
.frame(width: 400, height: 600) .frame(width: 400, height: 600)
} }
} }
+1 -1
View File
@@ -117,7 +117,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: String) -> String
return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.") return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.")
case .priv: case .priv:
let prof = profiles.lookup(id: pubkey) let prof = profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: prof, pubkey: pubkey).username let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50)
return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name) return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name)
case .non_zap: case .non_zap:
return NSLocalizedString("No zaps will be sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.") return NSLocalizedString("No zaps will be sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.")
+28
View File
@@ -0,0 +1,28 @@
//
// ZapUserView.swift
// damus
//
// Created by William Casarin on 2023-06-22.
//
import SwiftUI
struct ZapUserView: View {
let state: DamusState
let pubkey: String
var body: some View {
HStack(alignment: .center) {
Text("Zap")
.font(.title2)
UserView(damus_state: state, pubkey: pubkey, spacer: false)
}
}
}
struct ZapUserView_Previews: PreviewProvider {
static var previews: some View {
ZapUserView(state: test_damus_state(), pubkey: "anon")
}
}
+2 -2
View File
@@ -22,8 +22,8 @@ struct ZapsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
LazyVStack { LazyVStack {
ForEach(zaps.zaps, id: \.request.id) { zap in ForEach(zaps.zaps, id: \.request.ev.id) { zap in
ZapEvent(damus: state, zap: zap) ZapEvent(damus: state, zap: zap, is_top_zap: false)
.padding([.horizontal]) .padding([.horizontal])
} }
} }

Some files were not shown because too many files have changed in this diff Show More