Compare commits

...

121 Commits

Author SHA1 Message Date
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
William Casarin 185fba150f carousel: fix scroll-up bug 2023-05-29 16:09:16 -07:00
Ben Weeks 06ba0f7387 Add ability to show multiple posts per user in Universe
ChangeLog-Added: Add ability to show multiple posts per user in Universe
Closes: #1198
Fixes: #1189
2023-05-29 15:35:29 -07:00
William Casarin 51c4fa1e32 Initial redesign
Changelog-Changed: Redesign phase 1 (text, icons)
2023-05-29 15:31:07 -07:00
Ben Weeks ed4ef0e215 Tweaked action bar icon sizes. 2023-05-29 14:47:10 -07:00
Ben Weeks 973e9fe714 Custom iconography added for other areas of the app.
Changelog-Added: Custom iconography added for other areas of the app.
2023-05-29 14:47:05 -07:00
Ben Weeks d12281fcc5 Custom iconography for the left navigation.
ChangeLog-Added: Custom iconography for the left navigation.
2023-05-29 14:46:59 -07:00
Ben Weeks 156d885e6e Custom iconography for the tab buttons.
ChangeLog-Added: Custom iconography for the tab buttons.
2023-05-29 14:46:52 -07:00
ericholguin bf01d08ea0 setupview: remove damus white button implementation 2023-05-29 14:38:24 -07:00
ericholguin 548d8e49ec createaccount: ui: modify text field look 2023-05-29 14:38:24 -07:00
ericholguin 0fc38e5c05 createaccount: ui: change color of copy icon 2023-05-29 14:38:24 -07:00
ericholguin 028cab9cf6 createaccount: ui: replace damus white buttons with gradient style buttons 2023-05-29 14:38:24 -07:00
ericholguin 29068a40c8 createaccount: ui: remove damus gradient and white font color add background 2023-05-29 14:38:24 -07:00
ericholguin ff06cfaf0c createaccount: ui: refactor signup form 2023-05-29 14:38:24 -07:00
ericholguin eb72fb481c createaccount: remove username from the form 2023-05-29 14:38:24 -07:00
ericholguin 5b7339a0de createaccount: ui: move key text and profile picture selection into frame 2023-05-29 14:38:24 -07:00
ericholguin 06ba72a23d createaccount: ui: remove create account text 2023-05-29 14:38:24 -07:00
ericholguin 47d594af00 createaccount: ui: center pubkey text and add gradient style 2023-05-29 14:38:24 -07:00
ericholguin dd022a953c createaccount: ui: add navigation title and use general padding 2023-05-29 14:38:24 -07:00
ericholguin a4cbf6a12a createaccount: ui: include login prompt and add padding 2023-05-29 14:38:24 -07:00
ericholguin 62dfc24e58 createaccount: ui: change the look of the form label 2023-05-29 14:38:24 -07:00
ericholguin cf15bd3463 createaccount: ui: change the look of the form text field 2023-05-29 14:38:24 -07:00
ericholguin fd44a56f9b createaccount: ui: remove fixed color from back nav 2023-05-29 14:38:24 -07:00
ericholguin 9a9fb28a48 createaccount: ui: add login prompt view struct 2023-05-29 14:38:24 -07:00
ericholguin ba7f675300 createaccount: ui: remove damus white button and use gradient style button 2023-05-29 14:38:24 -07:00
ericholguin 8e84b446a5 createaccount: ui: remove damus gradient 2023-05-29 14:38:24 -07:00
ericholguin 8dbe2c728f loginview: ui: update login preview 2023-05-29 14:38:24 -07:00
ericholguin f975723c0f loginview: ui: include create account prompt view 2023-05-29 14:38:24 -07:00
ericholguin 8ed04f0b0e loginview: ui: add orange color and bold font to pubkey message 2023-05-29 14:38:24 -07:00
ericholguin e33cb3b1c3 loginview: ui: replace damus white button with gradient style button 2023-05-29 14:38:24 -07:00
ericholguin f171cfffe7 loginview: functionality: removed the toggle for hex npub - NEEDS UPDATING 2023-05-29 14:38:24 -07:00
ericholguin 46556c8bdc loginview: ui: replace existing views with new sign in header and entry views 2023-05-29 14:38:24 -07:00
ericholguin 10fec3cbf8 loginview: add create account prompt view struct 2023-05-29 14:38:24 -07:00
ericholguin 7250cf0d8f loginview: add sign in entry view struct 2023-05-29 14:38:24 -07:00
ericholguin 2bddd5ce92 loginview: add sign in header view struct 2023-05-29 14:38:24 -07:00
ericholguin 027b39caed loginview: ui: fix up key input textfield appearance 2023-05-29 14:38:24 -07:00
ericholguin 122d0e451e loginview: ui: replace damus gradient with new login background 2023-05-29 14:38:24 -07:00
ericholguin f99e311e58 eulaview: ui: use new eula background 2023-05-29 14:38:24 -07:00
ericholguin 3fbf3cc12e eulaview: ui: replace damus white buttons with new gradient style buttons 2023-05-29 14:38:24 -07:00
ericholguin 6682f792d8 eulaview: fix up formatting and use new eula variable 2023-05-29 14:38:24 -07:00
ericholguin 2e53ce3905 eulaview: create eula variable and update headers to bold 2023-05-29 14:38:24 -07:00
ericholguin c238b6e28f setupview: ui: remove old title, buttons, carousel and use new welcome, views, and lets get started button 2023-05-29 14:38:24 -07:00
ericholguin e99142a5b8 setupview: remove damus gradient and use new login background 2023-05-29 14:38:24 -07:00
ericholguin 5716578c20 setupview: add why we need nostr view 2023-05-29 14:38:24 -07:00
ericholguin ec35a413fe setupview: add what is nostr view 2023-05-29 14:38:24 -07:00
ericholguin 724773cb45 setupview: add learn about nostr link view 2023-05-29 14:38:24 -07:00
ericholguin 15acdef912 navigation: remove SetupState and references and update navigation links for onboarding flow 2023-05-29 14:38:24 -07:00
ericholguin 72cc4c1fd7 assets: added nostr logo image used in setup view 2023-05-29 14:38:24 -07:00
ericholguin 65f4227e9b assets: added lightbulb image used in setup view 2023-05-29 14:38:24 -07:00
ericholguin f0be5a5ad8 assets: added login header background 2023-05-29 14:38:24 -07:00
ericholguin 48855d5d41 assets: added background for the EULA 2023-05-29 14:38:24 -07:00
ericholguin 90c22fdabd components: added reusable gradient button style 2023-05-29 14:38:24 -07:00
ericholguin dfd1032cd8 gradients: add pink gradient 2023-05-29 14:38:24 -07:00
ericholguin a0e1e16f17 gradients: add damus logo gradient 2023-05-29 14:38:24 -07:00
Ben Weeks 020a00bf7e Updated UI to use custom font
Changelog-Changed: Updated UI to use custom font
2023-05-29 14:38:24 -07:00
William Casarin 297aaf86c6 carousel: only show blurhash when there is one images
Blurhash interacts strangely with carousel
2023-05-29 14:31:54 -07:00
William Casarin f6dd060580 fixup! Add new image carousel 2023-05-29 14:31:16 -07:00
William Casarin 12428d01ad preloader: only preload non-animated images to test animation bug fix 2023-05-29 14:22:54 -07:00
Ben Weeks d091543448 Add new image carousel
Changelog-Added: Added dots under image carousel
Changelof-Fixed: Fixed carousel issues with multiple images of different aspect ratios
Closes: #1004
2023-05-29 14:22:50 -07:00
Ben Weeks 2e596a47a1 Fix event menu capitalization
Closes: #1191
2023-05-29 13:23:24 -07:00
OlegAba ff15156297 Fix side menu offset
Changelog-Fixed: Fix side menu bug in landscape
Closes: #560
2023-05-29 13:17:05 -07:00
Ben Weeks 0a9fbf5031 Removed hex key to RGB functionality. 2023-05-29 12:42:58 -07:00
Ben Weeks 8eebc2abe5 Minor changes to profile ready for rebrand Key removal from nPub in profile 2023-05-29 12:42:58 -07:00
Ben Weeks c22199165a Use "Follow me on nostr" text when looking at someone else's QR code
Fixes: #1069
Closes: #1199
Changelog-Fixed: Use "Follow me on nostr" text when looking at someone else's QR code
2023-05-29 12:27:07 -07:00
gladiusKatana f7a0370824 Add .frame & .position modifiers to TextEntry using ScrollView geometry
Closes: #1201
Changelog-Fixed: Fix issue where cursor dissapears when typing long message
2023-05-29 12:22:44 -07:00
Bryan Montz fe4277e817 simplified call site usage of NostrFilter 2023-05-29 12:19:27 -07:00
William Casarin 6804fbb607 Attempt fix for randomly broken animated GIFs
Changelog-Fixed: Attempt fix for randomly broken animated gifs
2023-05-29 10:18:52 -07:00
William Casarin 05503024cc Profile Caching
Changelog-Added: Add profile caching
2023-05-26 12:11:43 -07:00
gladius e4e477a2ac Fix cursor jumping when pressing return
Changelog-Fixed: Fix cursor jumping when pressing return
Closes: #775
2023-05-26 11:20:40 -07:00
tyiu 59e7a42b5f Fix side menu label size so that translations in longer languages fit without wrapping
Changelog-Fixed: Fix side menu label size so that translations in longer languages fit without wrapping
Closes: #1175
2023-05-26 10:35:25 -07:00
tyiu 0552c24108 Add mention parsing and fine-grained text selection on description in ProfileView
Changelog-Added: Add mention parsing and fine-grained text selection on description in ProfileView
Closes: #1172
2023-05-26 10:35:18 -07:00
Bryan Montz 61303f49ad fix zap test 2023-05-26 07:32:18 -05:00
Bryan Montz e10dc93233 add damus_donation to profile data model and update tests 2023-05-26 07:02:14 -05:00
Bryan Montz ea73c5252d Merge branch 'master' into user-cache 2023-05-26 06:46:47 -05:00
Bryan Montz 7e963c9025 synchronize access to network_pull_date_cache with a queue, writes with barrier flag 2023-05-26 06:38:20 -05:00
Bryan Montz 61ff7da2ae make database write async at the call site and update tests 2023-05-25 09:09:50 -05:00
Bryan Montz 7259641e26 speed optimizations: cache network_pull_date in memory and ensure writes on background queue 2023-05-25 08:20:41 -05:00
gladiusKatana f9a572faa2 dynamically set .isScrollEnabled in TextViewWrapper (true if UserSearch is present) 2023-05-24 20:41:03 -07:00
gladiusKatana 0f805d7ea7 override .isScrollEnabled in TextViewWrapper (ie, set to false at UITextView creation) 2023-05-24 20:41:03 -07:00
William Casarin 52ca33ef6a script: fetch popular users 2023-05-24 18:26:18 -07:00
tyiu 1769b08147 Fix reaction notification title to be consistent with ReactionView
Changelog-Fixed: Fix reaction notification title to be consistent with ReactionView
Closes: #1137
2023-05-24 15:20:02 -07:00
Bryan Montz 812213ff2b Add Damus splash screen
Changelog-Updated: Add Damus splash screen
2023-05-24 15:17:08 -07:00
transifex-integration[bot] d6ecf14b55 Apply translations
Closes: #1134
2023-05-24 15:16:45 -07:00
William Casarin 47a74257c8 nwc debugging 2023-05-24 15:16:45 -07:00
William Casarin 1b161fefd0 nwc: make sure to support nostr+walletconnect scheme
not sure why we have 2 schemes
2023-05-15 12:54:42 -07:00
William Casarin 0b9a274e67 postbox: change initial retry_after from 2 to 10 seconds 2023-05-15 12:54:09 -07:00
William Casarin 2bbbb5db65 Fix a few bugs with donations 2023-05-15 12:53:36 -07:00
William Casarin bffa42a13a Supporter Badges 2023-05-15 11:57:37 -07:00
Bryan Montz 91113fbc6d allow models to fetch profiles when they get stale 2023-05-15 08:33:35 -05:00
Bryan Montz d58a1e0ba3 add ability to check the freshness of a PersistedProfile 2023-05-15 08:31:49 -05:00
Bryan Montz 3f7b0a4d6e add network pull date to PersistedProfile model for staleness checking 2023-05-15 08:29:06 -05:00
Bryan Montz bc315dd571 remove identical function 2023-05-15 07:33:49 -05:00
Bryan Montz 32431096f5 add tests for ProfileDatabase 2023-05-13 09:20:01 -05:00
Bryan Montz 6172347455 add ability to remove all cached Profiles in ProfileDatabase 2023-05-13 09:18:47 -05:00
Bryan Montz 31d327a085 add ability to retrieve count of cached Profiles 2023-05-13 09:18:23 -05:00
Bryan Montz b5ae7df795 add ability to change ProfileDatabase's storage URL 2023-05-13 09:17:41 -05:00
Bryan Montz 3b0bb48dd4 integrate ProfileDatabase with existing Profiles caching class 2023-05-12 07:21:59 -05:00
Bryan Montz 4646f0e23c add ProfileDatabase class to read and write profiles to disk 2023-05-12 07:21:25 -05:00
Bryan Montz 7027b7016c add two-way translation between existing Profile class and new PersistedProfile CoreData class 2023-05-12 07:19:43 -05:00
Bryan Montz 76c57af548 add managed object class to match new Profile CoreData model 2023-05-12 07:18:12 -05:00
Bryan Montz 7983157c38 add CoreData model for Profile 2023-05-12 07:15:30 -05:00
152 changed files with 4346 additions and 1223 deletions
+32
View File
@@ -1,3 +1,35 @@
## [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
+110 -2
View File
@@ -11,6 +11,7 @@
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 */; };
@@ -18,6 +19,7 @@
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 */; };
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,10 +51,13 @@
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 */; };
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; }; 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; };
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */; };
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */; };
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; }; 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; };
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; }; 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; };
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; }; 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; };
@@ -205,6 +210,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 */; };
@@ -265,11 +272,19 @@
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; };
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; }; 5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.swift */; }; 6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; }; 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
@@ -285,6 +300,7 @@
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 */; };
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 */; };
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; }; F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; };
@@ -323,6 +339,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>"; };
@@ -365,6 +382,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>"; };
@@ -440,10 +458,13 @@
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>"; };
4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; }; 4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; };
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupporterBadge.swift; sourceTree = "<group>"; };
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoldSupportGradient.swift; sourceTree = "<group>"; };
4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; }; 4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; }; 4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; };
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; }; 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; };
@@ -630,6 +651,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>"; };
@@ -694,11 +716,19 @@
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; }; 50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; }; 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; }; 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; };
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; }; 5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.swift; sourceTree = "<group>"; }; 6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; }; 643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
@@ -714,6 +744,7 @@
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>"; };
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>"; };
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; }; F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
@@ -733,6 +764,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;
@@ -875,6 +907,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>";
@@ -906,10 +939,20 @@
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */, 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */, 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */, 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
); );
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 = (
@@ -936,6 +979,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 */,
@@ -990,6 +1034,7 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */, 4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */, 4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */, 3AA247FE297E3D900090C62D /* RepostsView.swift */,
50DA11252A16A23F00236234 /* Launch.storyboard */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */, 5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */, 643EA5C7296B764E005081BB /* RelayFilterView.swift */,
); );
@@ -999,6 +1044,7 @@
4C75EFAB28049CC80006080F /* Nostr */ = { 4C75EFAB28049CC80006080F /* Nostr */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
501F8C5329FF5EE2001AFC1D /* CoreData */,
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */, 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
4C75EFA527FF87A20006080F /* Nostr.swift */, 4C75EFA527FF87A20006080F /* Nostr.swift */,
4C75EFAE28049D340006080F /* NostrFilter.swift */, 4C75EFAE28049D340006080F /* NostrFilter.swift */,
@@ -1009,6 +1055,7 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */, 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */, 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */, 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */, 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */, 4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */, 50088DA029E8271A008A1FDF /* WebSocket.swift */,
@@ -1041,6 +1088,9 @@
children = ( children = (
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */, 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */, 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */,
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
); );
path = Gradients; path = Gradients;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1092,6 +1142,7 @@
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */, 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */, 50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */, 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1218,6 +1269,8 @@
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */, 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */, 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
4C8D00C929DF80350036AF10 /* TruncatedText.swift */, 4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1293,6 +1346,7 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */, 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */, 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */, 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */, 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */, 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
@@ -1384,6 +1438,15 @@
path = Images; path = Images;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
501F8C5329FF5EE2001AFC1D /* CoreData */ = {
isa = PBXGroup;
children = (
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */,
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */,
);
path = CoreData;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = { 7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1428,6 +1491,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 */;
@@ -1532,6 +1596,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 = "";
@@ -1552,6 +1617,7 @@
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 */,
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 */,
@@ -1588,6 +1654,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 */,
@@ -1606,6 +1673,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 */,
@@ -1664,6 +1732,7 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */, 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */, 4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */, 4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */, 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
@@ -1728,8 +1797,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 */,
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 */,
@@ -1738,6 +1810,7 @@
4C363A9C282838B9006E126D /* EventRef.swift in Sources */, 4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */, 4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */, 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */,
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */, 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */, 4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */, 4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */,
@@ -1753,12 +1826,15 @@
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */, 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */, 4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */,
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */, 4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */, 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
@@ -1812,6 +1888,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */, 4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */, 4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
@@ -1830,6 +1907,7 @@
4C75EFB528049D790006080F /* Relay.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
@@ -1837,6 +1915,7 @@
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.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 */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1844,6 +1923,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */, 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
@@ -2128,7 +2208,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 = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2143,6 +2223,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -2175,7 +2256,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 = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2190,6 +2271,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -2347,6 +2429,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 */
@@ -2360,7 +2450,25 @@
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 */
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */,
);
currentVersion = 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */;
path = Damus.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
}; };
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
} }
@@ -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",
@@ -1,15 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "shaka-full.pdf", "filename" : "eula-bg.svg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }
+28
View File
@@ -0,0 +1,28 @@
<svg width="430" height="813" viewBox="0 0 430 813" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_1069_29012)">
<path d="M44.0811 256.851L186.315 111L276.203 223.574L244.02 388.295L69.9751 697.084L100.678 498.338L44.0811 256.851Z" fill="url(#paint0_linear_1069_29012)"/>
</g>
<g filter="url(#filter1_f_1069_29012)">
<path d="M116.509 587.348L206.677 479.401L230.746 273.265L266.666 231.183L367.424 396.975L281.292 659.008L266.665 801.413L66.889 763.694L116.509 587.348Z" fill="url(#paint1_linear_1069_29012)"/>
</g>
<defs>
<filter id="filter0_f_1069_29012" x="-66.6248" y="0.294121" width="453.534" height="807.496" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
</filter>
<filter id="filter1_f_1069_29012" x="-43.8172" y="120.477" width="521.947" height="791.642" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
</filter>
<linearGradient id="paint0_linear_1069_29012" x1="230.179" y1="166.577" x2="-67.7956" y2="310.108" gradientUnits="userSpaceOnUse">
<stop stop-color="#D34CD9"/>
<stop offset="1" stop-color="#4E4DF4"/>
</linearGradient>
<linearGradient id="paint1_linear_1069_29012" x1="139.483" y1="462.902" x2="377.854" y2="565.47" gradientUnits="userSpaceOnUse">
<stop stop-color="#0DE8FF"/>
<stop offset="1" stop-color="#641AAE"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,15 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "shaka-line.pdf", "filename" : "gradient.jpg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+12
View File
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "lightbulb.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+14
View File
@@ -0,0 +1,14 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.2" width="48" height="48" rx="24" fill="url(#paint0_linear_1843_42349)"/>
<path d="M19.9993 36.0001H27.9993M28.8095 28.0001C31.5199 26.3669 33.3327 23.3952 33.3327 20.0001C33.3327 14.8454 29.154 10.6667 23.9993 10.6667C18.8447 10.6667 14.666 14.8454 14.666 20.0001C14.666 23.3952 16.4788 26.3669 19.1892 28.0001M28.8095 28.0001C28.5475 28.1579 28.2772 28.3032 27.9993 28.4352V31.3334C27.9993 31.7016 27.7009 32.0001 27.3327 32.0001H20.666C20.2978 32.0001 19.9993 31.7016 19.9993 31.3334V28.4352C19.7215 28.3032 19.4512 28.1579 19.1892 28.0001M28.8095 28.0001H19.1892" stroke="url(#paint1_linear_1843_42349)" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_1843_42349" x1="5.41935" y1="0.774194" x2="37.9355" y2="47.2258" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9AD1C"/>
<stop offset="1" stop-color="#DF7E0C"/>
</linearGradient>
<linearGradient id="paint1_linear_1843_42349" x1="16.7735" y1="11.0754" x2="35.0141" y2="30.2759" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9AD1C"/>
<stop offset="1" stop-color="#DF7E0C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "header.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 34 KiB

+12
View File
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "nostr-logo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

@@ -1,88 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.073975 cm
1.000000 1.000000 1.000000 scn
1.295334 8.661732 m
3.613694 8.367855 l
4.475733 8.733568 5.268113 9.771931 5.474915 10.327032 c
6.083156 11.959681 5.507567 14.604573 5.474915 15.061715 c
5.448792 15.427428 6.008246 15.693006 6.291239 15.780080 c
7.571236 15.858447 8.508359 14.876789 8.642253 13.984165 c
8.740212 13.331103 8.576948 11.752880 8.381030 10.849482 c
8.979668 10.936556 10.980525 10.901726 11.868687 10.849482 c
12.756847 10.797236 13.474895 10.196423 14.193260 9.412750 c
14.767952 8.237244 13.953805 7.725680 13.474895 7.616838 c
13.834077 7.257654 l
14.781013 5.918882 13.649043 5.178749 13.115711 5.004600 c
13.474895 4.743376 l
14.487136 3.763786 13.246323 2.751544 13.017752 2.882155 c
11.058574 3.176033 l
15.499378 1.673996 l
16.054478 0.400530 15.074889 0.073999 14.781013 0.073999 c
8.576947 1.673996 l
6.291239 1.673996 5.311650 1.869914 4.299407 2.163791 c
4.157911 2.131138 3.659409 1.987464 2.797370 1.673996 c
1.935332 1.360527 1.219143 2.087601 0.968804 2.490320 c
-0.285071 4.083785 -0.467927 7.257655 1.295334 8.661732 c
h
f
n
Q
endstream
endobj
3 0 obj
1149
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 15.666626 15.710510 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001239 00000 n
0000001262 00000 n
0000001435 00000 n
0000001509 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1568
%%EOF
-323
View File
@@ -1,323 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.474731 -0.563965 cm
1.000000 1.000000 1.000000 scn
3.613694 9.332577 m
3.553993 8.861599 l
3.637261 8.851044 3.721838 8.862753 3.799107 8.895533 c
3.613694 9.332577 l
h
1.295334 9.626453 m
1.355035 10.097433 l
1.227973 10.113539 1.099794 10.077623 0.999601 9.997839 c
1.295334 9.626453 l
h
0.968804 3.455042 m
1.372000 3.705677 l
1.362764 3.720535 1.352713 3.734872 1.341894 3.748621 c
0.968804 3.455042 l
h
4.299407 3.128512 m
4.431771 3.584435 l
4.353942 3.607030 4.271623 3.609325 4.192656 3.591103 c
4.299407 3.128512 l
h
8.576947 2.638718 m
8.695503 3.098424 l
8.656776 3.108411 8.616942 3.113465 8.576947 3.113465 c
8.576947 2.638718 l
h
14.781013 1.038721 m
14.662457 0.579016 l
14.701184 0.569027 14.741018 0.563974 14.781013 0.563974 c
14.781013 1.038721 l
h
15.499378 2.638718 m
15.934578 2.828420 l
15.881091 2.951125 15.778289 3.045548 15.651489 3.088437 c
15.499378 2.638718 l
h
11.058574 4.140755 m
11.128998 4.610250 l
10.885809 4.646729 10.655017 4.491467 10.597156 4.252461 c
10.539293 4.013455 10.673516 3.769826 10.906463 3.691035 c
11.058574 4.140755 l
h
13.017752 3.846877 m
13.253292 4.259073 l
13.202273 4.288227 13.146286 4.307655 13.088176 4.316372 c
13.017752 3.846877 l
h
13.474895 5.708097 m
13.805044 6.049252 l
13.789093 6.064689 13.772079 6.078987 13.754128 6.092043 c
13.474895 5.708097 l
h
13.115711 5.969321 m
12.968349 6.420619 l
12.798800 6.365256 12.674588 6.219535 12.646772 6.043359 c
12.618958 5.867183 12.692234 5.690281 12.836478 5.585376 c
13.115711 5.969321 l
h
13.834077 8.222376 m
14.221668 8.496526 l
14.206144 8.518474 14.188784 8.539063 14.169774 8.558073 c
13.834077 8.222376 l
h
13.474895 8.581559 m
13.369680 9.044500 l
13.201114 9.006190 13.066693 8.879284 13.018762 8.713197 c
12.970830 8.547110 13.016963 8.368095 13.139197 8.245862 c
13.474895 8.581559 l
h
14.193260 10.377472 m
14.619765 10.585986 l
14.599768 10.626891 14.573989 10.664707 14.543221 10.698271 c
14.193260 10.377472 l
h
8.381030 11.814203 m
7.917068 11.914822 l
7.884080 11.762714 7.927746 11.604099 8.033934 11.490305 c
8.140121 11.376513 8.295343 11.321997 8.449365 11.344399 c
8.381030 11.814203 l
h
8.642253 14.948887 m
9.111748 15.019311 l
8.642253 14.948887 l
h
6.291239 16.744801 m
6.262227 17.218662 l
6.224693 17.216364 6.187564 17.209614 6.151623 17.198555 c
6.291239 16.744801 l
h
5.474915 16.026436 m
5.948456 16.060261 l
5.474915 16.026436 l
h
5.474915 11.291754 m
5.030037 11.457493 l
5.474915 11.291754 l
h
3.673396 9.803555 m
1.355035 10.097433 l
1.235632 9.155476 l
3.553993 8.861599 l
3.673396 9.803555 l
h
0.999601 9.997839 m
-0.029049 9.178730 -0.454726 7.875908 -0.474048 6.619066 c
-0.493367 5.362488 -0.110331 4.058727 0.595713 3.161463 c
1.341894 3.748621 l
0.794064 4.444821 0.458734 5.524729 0.475334 6.604470 c
0.491930 7.683949 0.856455 8.670100 1.591066 9.255068 c
0.999601 9.997839 l
h
0.565608 3.204407 m
0.721970 2.952868 1.013515 2.611341 1.407507 2.372385 c
1.811404 2.127421 2.357187 1.973489 2.959612 2.192553 c
2.635129 3.084882 l
2.375515 2.990478 2.132184 3.043347 1.899893 3.184233 c
1.657696 3.331126 1.465977 3.554496 1.372000 3.705677 c
0.565608 3.204407 l
h
2.959612 2.192553 m
3.816493 2.504146 4.293336 2.639887 4.406158 2.665923 c
4.192656 3.591103 l
4.022485 3.551832 3.502325 3.400227 2.635129 3.084882 c
2.959612 2.192553 l
h
4.167043 2.672591 m
5.229115 2.364247 6.254152 2.163970 8.576947 2.163970 c
8.576947 3.113465 l
6.328326 3.113465 5.394184 3.305025 4.431771 3.584435 c
4.167043 2.672591 l
h
8.458392 2.179011 m
14.662457 0.579016 l
14.899569 1.498427 l
8.695503 3.098424 l
8.458392 2.179011 l
h
14.781013 0.563974 m
15.036198 0.563974 15.495326 0.684875 15.814721 1.047266 c
16.180891 1.462728 16.264221 2.072176 15.934578 2.828420 c
15.064179 2.449016 l
15.289635 1.931793 15.160722 1.741243 15.102402 1.675073 c
15.055794 1.622190 14.990156 1.579316 14.916806 1.549556 c
14.881134 1.535082 14.847747 1.525430 14.820526 1.519657 c
14.791491 1.513498 14.777695 1.513469 14.781013 1.513469 c
14.781013 0.563974 l
h
15.651489 3.088437 m
11.210685 4.590474 l
10.906463 3.691035 l
15.347267 2.188998 l
15.651489 3.088437 l
h
10.988150 3.671260 m
12.947328 3.377382 l
13.088176 4.316372 l
11.128998 4.610250 l
10.988150 3.671260 l
h
12.782211 3.434681 m
12.991495 3.315090 13.204453 3.370091 13.288217 3.396689 c
13.400116 3.432221 13.506123 3.490767 13.598186 3.554502 c
13.783985 3.683133 13.977411 3.877748 14.120350 4.119644 c
14.264680 4.363894 14.369576 4.678114 14.335162 5.031647 c
14.300108 5.391746 14.125634 5.739002 13.805044 6.049252 c
13.144745 5.366943 l
13.330275 5.187398 13.380290 5.040778 13.390134 4.939653 c
13.400617 4.831963 13.370820 4.717613 13.302905 4.602680 c
13.233600 4.485394 13.137231 4.390213 13.057724 4.335170 c
13.017135 4.307070 12.996612 4.300308 13.000857 4.301657 c
13.003194 4.302399 13.024761 4.309311 13.061064 4.310122 c
13.095938 4.310902 13.170414 4.306433 13.253292 4.259073 c
12.782211 3.434681 l
h
13.754128 6.092043 m
13.394944 6.353267 l
12.836478 5.585376 l
13.195662 5.324152 l
13.754128 6.092043 l
h
13.263074 5.518023 m
13.593105 5.625790 14.123367 5.907292 14.433812 6.409482 c
14.595931 6.671733 14.696482 6.993351 14.669847 7.364054 c
14.643518 7.730516 14.495621 8.109214 14.221668 8.496526 c
13.446486 7.948226 l
13.646002 7.666152 13.711709 7.450294 13.722795 7.296009 c
13.733575 7.145966 13.695351 7.020646 13.626177 6.908748 c
13.474038 6.662641 13.171650 6.487002 12.968349 6.420619 c
13.263074 5.518023 l
h
14.169774 8.558073 m
13.810592 8.917255 l
13.139197 8.245862 l
13.498380 7.886679 l
14.169774 8.558073 l
h
13.580109 8.118617 m
13.896242 8.190466 14.344993 8.395787 14.624650 8.816864 c
14.929440 9.275781 14.963785 9.882310 14.619765 10.585986 c
13.766754 10.168959 l
13.997427 9.697128 13.912044 9.460121 13.833706 9.342171 c
13.730235 9.186377 13.532457 9.081495 13.369680 9.044500 c
13.580109 8.118617 l
h
14.543221 10.698271 m
13.820906 11.486253 12.989320 12.223852 11.896564 12.288132 c
11.840808 11.340275 l
12.524374 11.300065 13.128883 10.836036 13.843298 10.056674 c
14.543221 10.698271 l
h
11.896564 12.288132 m
11.441970 12.314873 10.711069 12.336796 10.019300 12.341186 c
9.341933 12.345484 8.654247 12.333687 8.312695 12.284006 c
8.449365 11.344399 l
8.706450 11.381794 9.318512 11.396118 10.013274 11.391710 c
10.693633 11.387392 11.407242 11.365778 11.840808 11.340275 c
11.896564 12.288132 l
h
8.844993 11.713585 m
8.948084 12.188952 9.040332 12.829445 9.094679 13.432834 c
9.147870 14.023395 9.169946 14.631327 9.111748 15.019311 c
8.172758 14.878462 l
8.212520 14.613384 8.201942 14.105675 8.149012 13.518009 c
8.097237 12.943172 8.009893 12.342852 7.917068 11.914822 c
8.844993 11.713585 l
h
9.111748 15.019311 m
8.944062 16.137217 7.805658 17.313158 6.262227 17.218662 c
6.320251 16.270941 l
7.336813 16.333179 8.072657 15.545805 8.172758 14.878462 c
9.111748 15.019311 l
h
6.151623 17.198555 m
5.976391 17.144638 5.715709 17.036982 5.490986 16.876261 c
5.292936 16.734617 4.969444 16.439627 5.001374 15.992612 c
5.948456 16.060261 l
5.951383 16.019283 5.934667 15.999795 5.943361 16.012491 c
5.954769 16.029152 5.984430 16.061831 6.043331 16.103956 c
6.162553 16.189222 6.323094 16.257891 6.430855 16.291048 c
6.151623 17.198555 l
h
5.001374 15.992612 m
5.011176 15.855374 5.059216 15.566318 5.104405 15.255149 c
5.152757 14.922197 5.207128 14.509316 5.241940 14.062993 c
5.312967 13.152368 5.295928 12.171200 5.030037 11.457493 c
5.919792 11.126015 l
6.262142 12.044956 6.261431 13.202559 6.188560 14.136827 c
6.151423 14.612950 6.093790 15.049047 6.044043 15.391605 c
5.991133 15.755945 5.954979 15.968927 5.948456 16.060261 c
5.001374 15.992612 l
h
5.030037 11.457493 m
4.953650 11.252455 4.742510 10.903708 4.434547 10.555828 c
4.127778 10.209298 3.769400 9.914337 3.428282 9.769621 c
3.799107 8.895533 l
4.320028 9.116529 4.788858 9.523607 5.145489 9.926461 c
5.500926 10.327968 5.789377 10.775953 5.919792 11.126015 c
5.030037 11.457493 l
h
f
n
Q
endstream
endobj
3 0 obj
7995
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.615845 16.660034 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000008085 00000 n
0000008108 00000 n
0000008281 00000 n
0000008355 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
8414
%%EOF
@@ -0,0 +1,45 @@
//
// GradientButtonStyle.swift
// damus
//
// Created by eric on 5/20/23.
//
import SwiftUI
struct GradientButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.padding()
.foregroundColor(Color.white)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(PinkGradient.gradient)
}
.scaleEffect(configuration.isPressed ? 0.8 : 1)
}
}
struct GradientButtonStyle_Previews: PreviewProvider {
static var previews: some View {
VStack {
Button("Dynamic Size", action: {
print("dynamic size")
})
.buttonStyle(GradientButtonStyle())
Button(action: {
print("infinite width")
}) {
HStack {
Text("Infinite Width")
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding()
}
}
}
@@ -0,0 +1,29 @@
//
// DamusLogoGradient.swift
// damus
//
// Created by eric on 5/24/23.
//
import SwiftUI
fileprivate let damus_logo_grad_c1 = hex_col(r: 0x30, g: 0xb3, b: 0xf1)
fileprivate let damus_logo_grad_c2 = hex_col(r: 0xc5, g: 0x39, b: 0xf9)
fileprivate let damus_logo_grad = [damus_logo_grad_c1, damus_logo_grad_c2]
struct DamusLogoGradient: View {
var body: some View {
DamusLogoGradient.gradient
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: damus_logo_grad, startPoint: .leading, endPoint: .trailing)
}
}
struct DamusLogoGradient_Previews: PreviewProvider {
static var previews: some View {
DamusLogoGradient()
}
}
@@ -0,0 +1,29 @@
//
// GoldSupportGradient.swift
// damus
//
// Created by William Casarin on 2023-05-15.
//
import SwiftUI
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
let GoldGradient: LinearGradient =
LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
struct GoldGradientView: View {
var body: some View {
GoldGradient
.edgesIgnoringSafeArea([.top,.bottom])
}
}
struct GoldGradientView_Previews: PreviewProvider {
static var previews: some View {
GoldGradientView()
}
}
@@ -0,0 +1,30 @@
//
// PinkGradient.swift
// damus
//
// Created by eric on 5/20/23.
//
import SwiftUI
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x4c, b: 0xd9)
fileprivate let damus_grad_c2 = hex_col(r: 0xf8, g: 0x69, b: 0xb6)
fileprivate let pink_grad = [damus_grad_c1, damus_grad_c2]
struct PinkGradient: View {
var body: some View {
PinkGradient.gradient
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
}
}
struct PinkGradient_Previews: PreviewProvider {
static var previews: some View {
PinkGradient()
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ struct IconLabel: View {
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: img_name) Image(img_name)
.foregroundColor(img_color) .foregroundColor(img_color)
.frame(width: 20) .frame(width: 20)
.padding([.trailing], 20) .padding([.trailing], 20)
+111 -24
View File
@@ -52,8 +52,9 @@ enum ImageShape {
} }
} }
// MARK: - Image Carousel
struct ImageCarousel: View { struct ImageCarousel: View {
var urls: [URL] var urls: [MediaUrl]
let evid: String let evid: String
@@ -63,13 +64,18 @@ struct ImageCarousel: View {
@State private var current_url: URL? = nil @State private var current_url: URL? = nil
@State private var image_fill: ImageFill? = nil @State private var image_fill: ImageFill? = nil
let fillHeight: CGFloat = 350 @State private var fillHeight: CGFloat = 350
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2 @State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2 // 1.2
@State private var firstImageHeight: CGFloat? = nil
@State private var currentImageHeight: CGFloat?
@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
@@ -80,35 +86,64 @@ struct ImageCarousel: View {
} }
var height: CGFloat { var height: CGFloat {
image_fill?.height ?? fillHeight firstImageHeight ?? image_fill?.height ?? fillHeight
} }
func Placeholder(url: URL, geo_size: CGSize) -> some View { func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
Group { Group {
if let meta = state.events.lookup_img_metadata(url: url), if num_urls > 1 {
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
Color.clear
} else if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state { case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash) Image(uiImage: blurhash)
.resizable() .resizable()
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale) .frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
} else { } else {
EmptyView() Color.clear
} }
} }
.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 body: some View { func video_model(_ url: URL) -> VideoPlayerModel {
TabView { return state.events.get_video_player_model(url: url)
ForEach(urls, id: \.absoluteString) { url in }
GeometryReader { geo in
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) KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background))) .callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true) .backgroundDecode(true)
@@ -119,7 +154,7 @@ struct ImageCarousel: View {
view.framePreloadCount = 3 view.framePreloadCount = 3
} }
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in .imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.previews.cache_image_meta(evid: evid, image_fill: fill) state.events.get_cache_data(evid).media_metadata_model.fill = fill
// blur hash can be discarded when we have the url // blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove // NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen // it when the image is loaded in memory. This may happen
@@ -128,27 +163,77 @@ struct ImageCarousel: View {
state.events.lookup_img_metadata(url: url)?.state = .not_needed state.events.lookup_img_metadata(url: url)?.state = .not_needed
} }
image_fill = fill image_fill = fill
if index == 0 {
firstImageHeight = fill.height
//maxHeight = firstImageHeight ?? maxHeight
} else {
//maxHeight = firstImageHeight ?? fill.height
}
} }
.background { .background {
Placeholder(url: url, geo_size: geo.size) Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
} }
.aspectRatio(contentMode: filling ? .fill : .fit) .aspectRatio(contentMode: filling ? .fill : .fit)
.position(x: geo.size.width / 2, y: geo.size.height / 2)
.tabItem { .tabItem {
Text(url.absoluteString) Text(url.absoluteString)
} }
.id(url.absoluteString) .id(url.absoluteString)
.padding(0)
}
var Medias: some View {
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
GeometryReader { geo in
Media(geo: geo, url: urls[index], index: index)
} }
} }
} }
.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: self.height) .frame(height: height)
.onTapGesture { .onChange(of: selectedIndex) { value in
open_sheet = true selectedIndex = value
} }
.tabViewStyle(PageTabViewStyle()) .tabViewStyle(PageTabViewStyle())
} }
var body: some View {
VStack {
Medias
.onTapGesture { }
// This is our custom carousel image indicator
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
}
}
}
// MARK: - Custom Carousel
struct CarouselDotsView<T>: View {
let urls: [T]
@Binding var selectedIndex: Int
var body: some View {
if urls.count > 1 {
HStack {
ForEach(urls.indices, id: \.self) { index in
Circle()
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
.frame(width: 10, height: 10)
.onTapGesture {
selectedIndex = index
}
}
}
.padding(.top, CGFloat(8))
.id(UUID())
}
}
} }
// MARK: - Image Modifier // MARK: - Image Modifier
@@ -199,9 +284,11 @@ public struct ImageFill {
} }
} }
// 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])
} }
} }
+3 -3
View File
@@ -25,10 +25,10 @@ struct InvoiceView: View {
UIPasteboard.general.string = invoice.string UIPasteboard.general.string = invoice.string
} label: { } label: {
if !copied { if !copied {
Image(systemName: "doc.on.clipboard") Image("copy2")
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Image(systemName: "checkmark.circle") Image("check-circle")
.foregroundColor(DamusColors.green) .foregroundColor(DamusColors.green)
} }
} }
@@ -63,7 +63,7 @@ struct InvoiceView: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack { HStack {
Label("", systemImage: "bolt.fill") Label("", image: "zap.fill")
.foregroundColor(.orange) .foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.") Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
Spacer() Spacer()
+2 -2
View File
@@ -32,11 +32,11 @@ struct NIP05Badge: View {
Group { Group {
if nip05_color { if nip05_color {
LINEAR_GRADIENT LINEAR_GRADIENT
.mask(Image(systemName: "checkmark.seal.fill") .mask(Image("check-circle.fill")
.resizable() .resizable()
).frame(width: 14, height: 14) ).frame(width: 14, height: 14)
} else if show_domain { } else if show_domain {
Image(systemName: "checkmark.seal.fill") Image("check-circle.fill")
.font(.footnote) .font(.footnote)
.nip05_colorized(gradient: nip05_color) .nip05_colorized(gradient: nip05_color)
} }
+1 -1
View File
@@ -14,7 +14,7 @@ struct Reposted: View {
var body: some View { var body: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
Image(systemName: "arrow.2.squarepath") Image("repost")
.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)
+73
View File
@@ -0,0 +1,73 @@
//
// SupporterBadge.swift
// damus
//
// Created by William Casarin on 2023-05-15.
//
import SwiftUI
struct SupporterBadge: View {
let percent: Int
let size: CGFloat = 17
var body: some View {
if percent < 100 {
Image("star.fill")
.resizable()
.frame(width:size, height:size)
.foregroundColor(support_level_color(percent))
} else {
Image("star.fill")
.resizable()
.frame(width:size, height:size)
.foregroundStyle(GoldGradient)
}
}
}
func support_level_color(_ percent: Int) -> Color {
if percent == 0 {
return .gray
}
let percent_f = Double(percent) / 100.0
let cutoff = 0.5
let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
let s = 0.9; // Saturation
let b = 0.9; // Brightness
return Color(hue: h, saturation: s, brightness: b)
}
struct SupporterBadge_Previews: PreviewProvider {
static func Level(_ p: Int) -> some View {
HStack(alignment: .center) {
SupporterBadge(percent: p)
.frame(width: 50)
Text(verbatim: p.formatted())
.frame(width: 50)
}
}
static var previews: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
Level(1)
Level(10)
Level(20)
Level(30)
Level(40)
Level(50)
}
Level(60)
Level(70)
Level(80)
Level(90)
Level(100)
}
}
}
+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 {
+3 -1
View File
@@ -43,7 +43,9 @@ struct UserView: View {
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 = profile?.about {
Text(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) .lineLimit(3)
.font(.footnote) .font(.footnote)
} }
+3 -1
View File
@@ -13,7 +13,7 @@ struct WebsiteLink: View {
var body: some View { var body: some View {
HStack { HStack {
Image(systemName: "link") Image("link")
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.footnote) .font(.footnote)
@@ -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)
}) })
} }
} }
+43 -25
View File
@@ -10,29 +10,25 @@ 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
@@ -45,11 +41,11 @@ struct ZapButton: View {
var zap_img: String { var zap_img: String {
switch our_zap { switch our_zap {
case .none: case .none:
return "bolt" return "zap"
case .zap: case .zap:
return "bolt.fill" return "zap.fill"
case .pending: case .pending:
return "bolt.fill" return "zap.fill"
} }
} }
@@ -71,7 +67,7 @@ struct ZapButton: View {
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
} }
@@ -120,9 +116,12 @@ struct ZapButton: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Button(action: { Button(action: {
}, label: { }, label: {
Image(systemName: zap_img) Image(zap_img)
.resizable()
.foregroundColor(zap_color) .foregroundColor(zap_color)
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
}) })
if zaps.zap_total > 0 { if zaps.zap_total > 0 {
@@ -139,7 +138,7 @@ struct ZapButton: View {
tap() tap()
}) })
.sheet(isPresented: $button.showing_zap_customizer) { .sheet(isPresented: $button.showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl) CustomizeZapView(state: damus_state, target: target, lnurl: lnurl)
} }
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) { .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 ?? "") SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
@@ -147,7 +146,7 @@ struct ZapButton: View {
.onReceive(handle_notify(.zapping)) { notif in .onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else { guard zap_ev.target.id == self.target.id else {
return return
} }
@@ -166,6 +165,8 @@ struct ZapButton: View {
let wallet = damus_state.settings.default_wallet.model let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv) open_with_wallet(wallet: wallet, invoice: inv)
} }
case .sent_from_nwc:
break
} }
} }
} }
@@ -177,7 +178,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)
} }
} }
@@ -193,14 +194,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 {
@@ -228,7 +228,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
@@ -242,7 +242,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
@@ -255,28 +255,46 @@ 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
} }
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: .once({ pe in var flusher: OnFlush? = nil
// Don't donate on custom zaps
if !is_custom && damus_state.settings.donation_percent > 0 {
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.init { @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)
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)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return return
} }
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
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)
} }
} }
+17 -7
View File
@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AVKit
struct TimestampedProfile { struct TimestampedProfile {
let profile: Profile let profile: Profile
@@ -291,7 +292,7 @@ struct ContentView: View {
self.active_sheet = .filter self.active_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."), systemImage: "line.3.horizontal.decrease") Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
.foregroundColor(.gray) .foregroundColor(.gray)
//.contentShape(Rectangle()) //.contentShape(Rectangle())
} }
@@ -315,6 +316,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
@@ -397,7 +399,7 @@ struct ContentView: View {
return return
} }
ds.postbox.send(ev) ds.postbox.send(ev)
if let profile = ds.profiles.profiles[ev.pubkey] { if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
ds.postbox.send(profile.event) ds.postbox.send(profile.event)
} }
} }
@@ -455,6 +457,11 @@ struct ContentView: View {
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 +476,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 +509,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 +569,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.")
@@ -692,7 +702,7 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
let kinds = filter.kinds ?? [] let kinds = filter.kinds ?? []
let initial: Int64? = nil let initial: Int64? = nil
let earliest = kinds.reduce(initial) { earliest, kind in let earliest = kinds.reduce(initial) { earliest, kind in
let last = last_of_kind[kind] let last = last_of_kind[kind.rawValue]
let since: Int64? = get_since_time(last_event: last) let since: Int64? = get_since_time(last_event: last)
if earliest == nil { if earliest == nil {
@@ -746,10 +756,10 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
var has_event = false var has_event = false
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ]) var filter = search_type == .event ? NostrFilter(ids: [evid]) : NostrFilter(authors: [evid])
if search_type == .profile { if search_type == .profile {
filter.kinds = [NostrKind.metadata.rawValue] filter.kinds = [.metadata]
} }
filter.limit = 1 filter.limit = 1
+1 -1
View File
@@ -24,7 +24,7 @@ class EventsModel: ObservableObject {
} }
private func get_filter() -> NostrFilter { private func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([kind.rawValue]) var filter = NostrFilter(kinds: [kind])
filter.referenced_ids = [target] filter.referenced_ids = [target]
filter.limit = 500 filter.limit = 500
return filter return filter
+5 -7
View File
@@ -30,9 +30,8 @@ class FollowersModel: ObservableObject {
} }
func get_filter() -> NostrFilter { func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_contacts NostrFilter(kinds: [.contacts],
filter.pubkeys = [target] pubkeys: [target])
return filter
} }
func subscribe() { func subscribe() {
@@ -56,14 +55,13 @@ class FollowersModel: ObservableObject {
} }
func load_profiles(relay_id: String) { func load_profiles(relay_id: String) {
var filter = NostrFilter.filter_profiles let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
if authors.isEmpty { if authors.isEmpty {
return return
} }
filter.authors = authors let filter = NostrFilter(kinds: [.metadata],
authors: authors)
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event) damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
} }
+2 -2
View File
@@ -21,10 +21,10 @@ class FollowingModel {
} }
func get_filter() -> NostrFilter { func get_filter() -> NostrFilter {
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue]) var f = NostrFilter(kinds: [.metadata])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have // don't fetch profiles we already have
if damus_state.profiles.lookup(id: pk) != nil { if damus_state.profiles.has_fresh_profile(id: pk) {
return return
} }
acc.append(pk) acc.append(pk)
+59 -39
View File
@@ -132,11 +132,11 @@ class HomeModel: ObservableObject {
case .nwc_request: case .nwc_request:
break break
case .nwc_response: case .nwc_response:
handle_nwc_response(ev) handle_nwc_response(ev, relay: relay_id)
} }
} }
func handle_nwc_response(_ ev: NostrEvent) { func handle_nwc_response(_ ev: NostrEvent, relay: String) {
Task { @MainActor in Task { @MainActor in
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time // TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect, guard let nwc_str = damus_state.settings.nostr_wallet_connect,
@@ -148,25 +148,24 @@ class HomeModel: ObservableObject {
// since command results are not returned for ephemeral events, // since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over // remove the request from the postbox which is likely failing over and over
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) { if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
print("nwc: got response, removed \(resp.req_id) from the postbox") print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
} else { } else {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove") print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
}
if resp.response.error == nil {
} }
guard let err = resp.response.error else { guard let err = resp.response.error else {
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp) nwc_success(state: self.damus_state, resp: resp)
return return
} }
print("nwc error: \(err)") print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
} }
} }
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) { 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 { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return return
} }
@@ -188,7 +187,12 @@ class HomeModel: ObservableObject {
} }
if damus_state.settings.zap_notification { if damus_state.settings.zap_notification {
// Create in-app local notification for zap received. // Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "") switch zap.target {
case .profile(let profile_id):
create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id)
case .note(let note_target):
create_in_app_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_id)
}
} }
} }
@@ -398,10 +402,9 @@ 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.filter_contacts var filter = NostrFilter(kinds: [.contacts],
filter.authors = [self.damus_state.pubkey] limit: 1,
filter.limit = 1 authors: [damus_state.pubkey])
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
} }
@@ -412,23 +415,19 @@ class HomeModel: ObservableObject {
var friends = damus_state.contacts.get_friend_list() var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey) friends.append(damus_state.pubkey)
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue]) var contacts_filter = NostrFilter(kinds: [.metadata])
contacts_filter.authors = friends contacts_filter.authors = friends
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue]) var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
our_contacts_filter.authors = [damus_state.pubkey] our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue]) var our_blocklist_filter = NostrFilter(kinds: [.list])
our_blocklist_filter.parameter = ["mute"] our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey] our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter.filter_kinds([ var dms_filter = NostrFilter(kinds: [.dm])
NostrKind.dm.rawValue,
])
var our_dms_filter = NostrFilter.filter_kinds([ var our_dms_filter = NostrFilter(kinds: [.dm])
NostrKind.dm.rawValue,
])
// friends only?... // friends only?...
//dms_filter.authors = friends //dms_filter.authors = friends
@@ -437,27 +436,27 @@ class HomeModel: ObservableObject {
our_dms_filter.authors = [ damus_state.pubkey ] our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes? // TODO: separate likes?
var home_filter_kinds = [ var home_filter_kinds: [NostrKind] = [
NostrKind.text.rawValue, .text,
NostrKind.boost.rawValue .boost
] ]
if !damus_state.settings.onlyzaps_mode { if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(NostrKind.like.rawValue) home_filter_kinds.append(.like)
} }
var home_filter = NostrFilter.filter_kinds(home_filter_kinds) var home_filter = NostrFilter(kinds: home_filter_kinds)
// include our pubkey as well even if we're not technically a friend // include our pubkey as well even if we're not technically a friend
home_filter.authors = friends home_filter.authors = friends
home_filter.limit = 500 home_filter.limit = 500
var notifications_filter_kinds = [ var notifications_filter_kinds: [NostrKind] = [
NostrKind.text.rawValue, .text,
NostrKind.boost.rawValue, .boost,
NostrKind.zap.rawValue, .zap,
] ]
if !damus_state.settings.onlyzaps_mode { if !damus_state.settings.onlyzaps_mode {
notifications_filter_kinds.append(NostrKind.like.rawValue) notifications_filter_kinds.append(.like)
} }
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds) var notifications_filter = NostrFilter(kinds: notifications_filter_kinds)
notifications_filter.pubkeys = [damus_state.pubkey] notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500 notifications_filter.limit = 500
@@ -735,7 +734,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
var old_nip05: String? = nil var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
old_nip05 = mprof.profile.nip05 old_nip05 = mprof.profile.nip05
if mprof.timestamp > ev.created_at { if mprof.event.created_at > ev.created_at {
// skip if we already have an newer profile // skip if we already have an newer profile
return return
} }
@@ -752,7 +751,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
print("validated nip05 for '\(nip05)'") print("validated nip05 for '\(nip05)'")
} }
DispatchQueue.main.async { Task { @MainActor in
profiles.validated[ev.pubkey] = validated profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
@@ -1113,7 +1112,7 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
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)
@@ -1124,7 +1123,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)
@@ -1203,12 +1223,12 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName) title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification" identifier = "myBoostNotification"
case .like: case .like:
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content) title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
identifier = "myLikeNotification" identifier = "myLikeNotification"
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
} }
+1 -1
View File
@@ -246,7 +246,7 @@ func format_msats_abbrev(_ msats: Int64) -> String {
formatter.positiveSuffix = "m" formatter.positiveSuffix = "m"
formatter.positivePrefix = "" formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0 formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2 formatter.maximumFractionDigits = 3
formatter.roundingMode = .down formatter.roundingMode = .down
formatter.roundingIncrement = 0.1 formatter.roundingIncrement = 0.1
formatter.multiplier = 1 formatter.multiplier = 1
+2 -9
View File
@@ -69,16 +69,9 @@ class ProfileModel: ObservableObject, Equatable {
} }
func subscribe() { func subscribe() {
var text_filter = NostrFilter.filter_kinds([ var text_filter = NostrFilter(kinds: [.text, .chat])
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
])
var profile_filter = NostrFilter.filter_kinds([ var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
NostrKind.contacts.rawValue,
NostrKind.metadata.rawValue,
NostrKind.boost.rawValue,
])
profile_filter.authors = [pubkey] profile_filter.authors = [pubkey]
+9 -32
View File
@@ -18,6 +18,7 @@ class SearchHomeModel: ObservableObject {
let base_subid = UUID().description let base_subid = UUID().description
let profiles_subid = UUID().description let profiles_subid = UUID().description
let limit: UInt32 = 250 let limit: UInt32 = 250
//let multiple_events_per_pubkey: Bool = false
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
@@ -27,7 +28,7 @@ class SearchHomeModel: ObservableObject {
} }
func get_base_filter() -> NostrFilter { func get_base_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue]) var filter = NostrFilter(kinds: [.text, .chat])
filter.limit = self.limit filter.limit = self.limit
filter.until = Int64(Date.now.timeIntervalSince1970) filter.until = Int64(Date.now.timeIntervalSince1970)
return filter return filter
@@ -60,7 +61,7 @@ class SearchHomeModel: ObservableObject {
return return
} }
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) { if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) { if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
return return
} }
seen_pubkey.insert(ev.pubkey) seen_pubkey.insert(ev.pubkey)
@@ -90,20 +91,6 @@ class SearchHomeModel: ObservableObject {
} }
} }
func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
var pubkeys = Set<String>()
for pk in event_pubkeys {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] { func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
switch load { switch load {
case .from_events(let events): case .from_events(let events):
@@ -114,17 +101,7 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
} }
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] { func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
var pubkeys = Set<String>() Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
} }
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] { func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
@@ -132,11 +109,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
for ev in events { for ev in events {
// lookup profiles from boosted events // lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil { if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey) pubkeys.insert(bev.pubkey)
} }
if profiles.lookup(id: ev.pubkey) == nil { if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey) pubkeys.insert(ev.pubkey)
} }
} }
@@ -150,16 +127,16 @@ enum PubkeysToLoad {
} }
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) { func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events) let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
filter.authors = authors
guard !authors.isEmpty else { guard !authors.isEmpty else {
return return
} }
print("loading \(authors.count) profiles from \(relay_id)") print("loading \(authors.count) profiles from \(relay_id)")
let filter = NostrFilter(kinds: [.metadata],
authors: authors)
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in
let (sid, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: conn_ev) { sub_id, ev in let (sid, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: conn_ev) { sub_id, ev in
guard sub_id == profiles_subid else { guard sub_id == profiles_subid else {
+1 -1
View File
@@ -34,7 +34,7 @@ class SearchModel: ObservableObject {
func subscribe() { func subscribe() {
// since 1 month // since 1 month
search.limit = self.limit search.limit = self.limit
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue] search.kinds = [.text, .like]
//likes_filter.ids = ref_events.referenced_ids! //likes_filter.ids = ref_events.referenced_ids!
+3 -3
View File
@@ -56,16 +56,16 @@ class ThreadModel: ObservableObject {
let thread_id = event.thread_id(privkey: nil) let thread_id = event.thread_id(privkey: nil)
ref_events.referenced_ids = [thread_id, event.id] ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [NostrKind.text.rawValue] ref_events.kinds = [.text]
ref_events.limit = 1000 ref_events.limit = 1000
event_filter.ids = [thread_id, event.id] event_filter.ids = [thread_id, event.id]
meta_events.referenced_ids = [event.id] meta_events.referenced_ids = [event.id]
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue] var kinds: [NostrKind] = [.zap, .text, .boost]
if !damus_state.settings.onlyzaps_mode { if !damus_state.settings.onlyzaps_mode {
kinds.append(NostrKind.like.rawValue) kinds.append(.like)
} }
meta_events.kinds = kinds meta_events.kinds = kinds
+3
View File
@@ -138,6 +138,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "show_only_preferred_languages", default_value: false) @Setting(key: "show_only_preferred_languages", default_value: false)
var show_only_preferred_languages: Bool var show_only_preferred_languages: Bool
@Setting(key: "multiple_events_per_pubkey", default_value: false)
var multiple_events_per_pubkey: Bool
@Setting(key: "onlyzaps_mode", default_value: false) @Setting(key: "onlyzaps_mode", default_value: false)
var onlyzaps_mode: Bool var onlyzaps_mode: Bool
+3
View File
@@ -16,6 +16,7 @@ enum WalletConnectState {
class WalletModel: ObservableObject { class WalletModel: ObservableObject {
var settings: UserSettingsStore var settings: UserSettingsStore
private(set) var previous_state: WalletConnectState private(set) var previous_state: WalletConnectState
var inital_percent: Int
@Published private(set) var connect_state: WalletConnectState @Published private(set) var connect_state: WalletConnectState
@@ -23,6 +24,7 @@ class WalletModel: ObservableObject {
self.connect_state = state self.connect_state = state
self.previous_state = .none self.previous_state = .none
self.settings = settings self.settings = settings
self.inital_percent = settings.donation_percent
} }
init(settings: UserSettingsStore) { init(settings: UserSettingsStore) {
@@ -35,6 +37,7 @@ class WalletModel: ObservableObject {
self.previous_state = .none self.previous_state = .none
self.connect_state = .none self.connect_state = .none
} }
self.inital_percent = settings.donation_percent
} }
func cancel() { func cancel() {
+15
View File
@@ -0,0 +1,15 @@
//
// ZapButtonModel.swift
// damus
//
// Created by Terry Yiu on 6/1/23.
//
import Foundation
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
}
+1 -1
View File
@@ -24,7 +24,7 @@ class ZapsModel: ObservableObject {
} }
func subscribe() { func subscribe() {
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue]) var filter = NostrFilter(kinds: [.zap])
switch target { switch target {
case .profile(let profile_id): case .profile(let profile_id):
filter.pubkeys = [profile_id] filter.pubkeys = [profile_id]
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="banner" optional="YES" attributeType="String"/>
<attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="display_name" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lud06" optional="YES" attributeType="String"/>
<attribute name="lud16" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="picture" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
</entity>
</model>
@@ -0,0 +1,39 @@
//
// PersistedProfile.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
@objc(PersistedProfile)
final class PersistedProfile: NSManagedObject {
@NSManaged var id: String?
@NSManaged var name: String?
@NSManaged var display_name: String?
@NSManaged var about: String?
@NSManaged var picture: String?
@NSManaged var banner: String?
@NSManaged var website: String?
@NSManaged var lud06: String?
@NSManaged var lud16: String?
@NSManaged var nip05: String?
@NSManaged var damus_donation: Int16
@NSManaged var last_update: Date? // The date that the profile was last updated by the user
@NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
func copyValues(from profile: Profile) {
name = profile.name
display_name = profile.display_name
about = profile.about
picture = profile.picture
banner = profile.banner
website = profile.website
lud06 = profile.lud06
lud16 = profile.lud16
nip05 = profile.nip05
damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
}
}
+13
View File
@@ -24,6 +24,19 @@ class Profile: Codable {
self.damus_donation = damus_donation self.damus_donation = damus_donation
} }
convenience init(persisted_profile: PersistedProfile) {
self.init(name: persisted_profile.name,
display_name: persisted_profile.display_name,
about: persisted_profile.about,
picture: persisted_profile.picture,
banner: persisted_profile.banner,
website: persisted_profile.website,
lud06: persisted_profile.lud06,
lud16: persisted_profile.lud16,
nip05: persisted_profile.nip05,
damus_donation: Int(persisted_profile.damus_donation))
}
private func str(_ str: String) -> String? { private func str(_ str: String) -> String? {
return get_val(str) return get_val(str)
} }
+24 -2
View File
@@ -492,11 +492,11 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
return ev return ev
} }
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent { func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") } var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
tags.append(["e", liked.id]) tags.append(["e", liked.id])
tags.append(["p", liked.pubkey]) tags.append(["p", liked.pubkey])
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags) let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
ev.calculate_id() ev.calculate_id()
ev.sign(privkey: privkey) ev.sign(privkey: privkey)
@@ -966,6 +966,28 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
return nil return nil
} }
/**
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
If the known kind is not a `NostrKind.like`, it will return `nil`.
If the event content is an empty string or `+`, it will map that to a heart emoji.
If the event content is a "-", it will map that to a dislike 👎 emoji.
Otherwise, it will return the event content at face value without transforming it.
*/
func to_reaction_emoji(ev: NostrEvent) -> String? {
guard ev.known_kind == NostrKind.like else {
return nil
}
switch ev.content {
case "", "+":
return "❤️"
case "-":
return "👎"
default:
return ev.content
}
}
extension [ReferencedId] { extension [ReferencedId] {
var pRefs: [ReferencedId] { var pRefs: [ReferencedId] {
get { get {
+17 -25
View File
@@ -9,15 +9,15 @@ import Foundation
struct NostrFilter: Codable, Equatable { struct NostrFilter: Codable, Equatable {
var ids: [String]? var ids: [String]?
var kinds: [Int]? var kinds: [NostrKind]?
var referenced_ids: [String]? var referenced_ids: [String]?
var pubkeys: [String]? var pubkeys: [String]?
var since: Int64? var since: Int64?
var until: Int64? var until: Int64?
var limit: UInt32? var limit: UInt32?
var authors: [String]? var authors: [String]?
var hashtag: [String]? = nil var hashtag: [String]?
var parameter: [String]? = nil var parameter: [String]?
private enum CodingKeys : String, CodingKey { private enum CodingKeys : String, CodingKey {
case ids case ids
@@ -32,31 +32,23 @@ struct NostrFilter: Codable, Equatable {
case limit case limit
} }
init(ids: [String]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [String]? = nil, pubkeys: [String]? = nil, since: Int64? = nil, until: Int64? = nil, limit: UInt32? = nil, authors: [String]? = nil, hashtag: [String]? = nil) {
self.ids = ids
self.kinds = kinds
self.referenced_ids = referenced_ids
self.pubkeys = pubkeys
self.since = since
self.until = until
self.limit = limit
self.authors = authors
self.hashtag = hashtag
}
public static func copy(from: NostrFilter) -> NostrFilter { public static func copy(from: NostrFilter) -> NostrFilter {
return NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag) NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
} }
public static func filter_hashtag(_ htags: [String]) -> NostrFilter { public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() }) NostrFilter(hashtag: htags.map { $0.lowercased() })
}
public static func filter_ids(_ ids: [String]) -> NostrFilter {
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
}
public static var filter_profiles: NostrFilter {
return filter_kinds([NostrKind.metadata.rawValue])
}
public static var filter_contacts: NostrFilter {
return filter_kinds([NostrKind.contacts.rawValue])
}
public static func filter_authors(_ authors: [String]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: authors)
}
public static func filter_kinds(_ kinds: [Int]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: kinds, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation import Foundation
enum NostrKind: Int { enum NostrKind: Int, Codable {
case metadata = 0 case metadata = 0
case text = 1 case text = 1
case contacts = 3 case contacts = 3
+1 -1
View File
@@ -140,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
} }
if tag_is_hashtag(parts) { if tag_is_hashtag(parts) {
return .filter(NostrFilter.filter_hashtag([parts[1]])) return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
} }
if let rid = tag_to_refid(parts) { if let rid = tag_to_refid(parts) {
+181
View File
@@ -0,0 +1,181 @@
//
// ProfileDatabase.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
enum ProfileDatabaseError: Error {
case missing_context
case outdated_input
}
final class ProfileDatabase {
private let entity_name = "PersistedProfile"
private var persistent_container: NSPersistentContainer?
private var background_context: NSManagedObjectContext?
private let cache_url: URL
/// This queue is used to synchronize access to the network_pull_date_cache dictionary, which
/// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profile_db",
qos: .userInteractive,
attributes: .concurrent)
private var network_pull_date_cache = [String: Date]()
init(cache_url: URL = ProfileDatabase.profile_cache_url) {
self.cache_url = cache_url
set_up()
}
private static var profile_cache_url: URL {
(FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
}
private var persistent_store_description: NSPersistentStoreDescription {
let description = NSPersistentStoreDescription(url: cache_url)
description.type = NSSQLiteStoreType
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
return description
}
private var object_model: NSManagedObjectModel? {
guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
}
private func set_up() {
guard let object_model else {
print("⚠️ Warning: ProfileDatabase failed to load its object model")
return
}
persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
persistent_container?.persistentStoreDescriptions = [persistent_store_description]
persistent_container?.loadPersistentStores { _, error in
if let error {
print("WARNING: ProfileDatabase failed to load: \(error)")
}
}
persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
background_context = persistent_container?.newBackgroundContext()
background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
}
private func get_persisted(id: String, context: NSManagedObjectContext) -> PersistedProfile? {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return try? context.fetch(request).first
}
func get_network_pull_date(id: String) -> Date? {
var pull_date: Date?
queue.sync {
pull_date = network_pull_date_cache[id]
}
if let pull_date {
return pull_date
}
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
request.propertiesToFetch = ["network_pull_date"]
guard let profile = try? persistent_container?.viewContext.fetch(request).first else {
return nil
}
queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = profile.network_pull_date
}
return profile.network_pull_date
}
// MARK: - Public
/// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
/// is older than one we already have. Database writes occur on a background context for best performance.
/// - Parameters:
/// - id: Profile id (pubkey)
/// - profile: Profile object to be stored
/// - last_update: Date that the Profile was updated
func upsert(id: String, profile: Profile, last_update: Date) async throws {
guard let context = background_context else {
throw ProfileDatabaseError.missing_context
}
try await context.perform {
var persisted_profile: PersistedProfile?
if let profile = self.get_persisted(id: id, context: context) {
if let existing_last_update = profile.last_update, last_update < existing_last_update {
throw ProfileDatabaseError.outdated_input
} else {
persisted_profile = profile
}
} else {
persisted_profile = NSEntityDescription.insertNewObject(forEntityName: self.entity_name, into: context) as? PersistedProfile
persisted_profile?.id = id
}
persisted_profile?.copyValues(from: profile)
persisted_profile?.last_update = last_update
let pull_date = Date.now
persisted_profile?.network_pull_date = pull_date
self.queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = pull_date
}
try context.save()
}
}
func get(id: String) -> Profile? {
guard let container = persistent_container,
let profile = get_persisted(id: id, context: container.viewContext) else {
return nil
}
return Profile(persisted_profile: profile)
}
var count: Int {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
let count = try? persistent_container?.viewContext.count(for: request)
return count ?? 0
}
func remove_all_profiles() throws {
guard let context = background_context, let container = persistent_container else {
throw ProfileDatabaseError.missing_context
}
queue.async(flags: .barrier) {
self.network_pull_date_cache.removeAll()
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity_name)
let batch_delete_request = NSBatchDeleteRequest(fetchRequest: request)
batch_delete_request.resultType = .resultTypeObjectIDs
let result = try container.persistentStoreCoordinator.execute(batch_delete_request, with: context) as! NSBatchDeleteResult
// NSBatchDeleteRequest is an NSPersistentStoreRequest, which operates on disk. So now we'll manually update our in-memory context.
if let object_ids = result.result as? [NSManagedObjectID] {
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: object_ids
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
}
}
}
+41 -10
View File
@@ -6,44 +6,58 @@
// //
import Foundation import Foundation
import UIKit
class Profiles { class Profiles {
static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
/// This queue is used to synchronize access to the profiles dictionary, which /// This queue is used to synchronize access to the profiles dictionary, which
/// prevents data races from crashing the app. /// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profiles", private var queue = DispatchQueue(label: "io.damus.profiles",
qos: .userInteractive, qos: .userInteractive,
attributes: .concurrent) attributes: .concurrent)
var profiles: [String: TimestampedProfile] = [:] private var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:] var validated: [String: NIP05] = [:]
var nip05_pubkey: [String: String] = [:] var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:] var zappers: [String: String] = [:]
private let database = ProfileDatabase()
func is_validated(_ pk: String) -> NIP05? { func is_validated(_ pk: String) -> NIP05? {
return validated[pk] validated[pk]
}
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
return queue.sync {
return profiles.enumerated()
}
} }
func lookup_zapper(pubkey: String) -> String? { func lookup_zapper(pubkey: String) -> String? {
if let zapper = zappers[pubkey] { zappers[pubkey]
return zapper
}
return nil
} }
func add(id: String, profile: TimestampedProfile) { func add(id: String, profile: TimestampedProfile) {
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.profiles[id] = profile self.profiles[id] = profile
} }
Task {
do {
try await database.upsert(id: id, profile: profile.profile, last_update: Date(timeIntervalSince1970: TimeInterval(profile.timestamp)))
} catch {
print("⚠️ Warning: Profiles failed to save a profile: \(error)")
}
}
} }
func lookup(id: String) -> Profile? { func lookup(id: String) -> Profile? {
var profile: Profile?
queue.sync { queue.sync {
return profiles[id]?.profile profile = profiles[id]?.profile
} }
return profile ?? database.get(id: id)
} }
func lookup_with_timestamp(id: String) -> TimestampedProfile? { func lookup_with_timestamp(id: String) -> TimestampedProfile? {
@@ -51,6 +65,23 @@ class Profiles {
return profiles[id] return profiles[id]
} }
} }
func has_fresh_profile(id: String) -> Bool {
// check memory first
var profile: Profile?
queue.sync {
profile = profiles[id]?.profile
}
if profile != nil {
return true
}
// then disk
guard let pull_date = database.get_network_pull_date(id: id) else {
return false
}
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
}
} }
+45
View File
@@ -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]
@@ -194,6 +201,28 @@ class EventCache {
return image_metadata[url.absoluteString.lowercased()] return image_metadata[url.absoluteString.lowercased()]
} }
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
}
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 +286,7 @@ class EventCache {
private func prune() { private func prune() {
events = [:] events = [:]
video_meta = [:]
event_data = [:] event_data = [:]
replies.replies = [:] replies.replies = [:]
} }
@@ -370,6 +400,14 @@ func preload_image(url: URL) {
} }
} }
func is_animated_image(url: URL) -> Bool {
guard let ext = url.pathComponents.last?.split(separator: ".").last?.lowercased() else {
return false
}
return ext == "gif"
}
func preload_event(plan: PreloadPlan, state: DamusState) async { func preload_event(plan: PreloadPlan, state: DamusState) async {
var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts
let settings = state.settings let settings = state.settings
@@ -388,6 +426,13 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
} }
for url in arts.images { for url in arts.images {
guard !is_animated_image(url: url) else {
// jb55: I have a theory that animated images are not working with the preloader due
// to some disk-cache write race condition. normal images need not apply
continue
}
preload_image(url: url) preload_image(url: url)
} }
} }
+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
} }
+6 -1
View File
@@ -42,7 +42,7 @@ class PostedEvent {
self.on_flush = on_flush self.on_flush = on_flush
self.flushed_once = false self.flushed_once = false
self.remaining = remaining.map { self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 2.0) Relayer(relay: $0, attempts: 0, retry_after: 10.0)
} }
} }
} }
@@ -150,6 +150,11 @@ class PostBox {
relayer.attempts += 1 relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970) relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5 relayer.retry_after *= 1.5
if let relay = pool.get_relay(relayer.relay) {
print("flushing event \(event.event.id) to \(relayer.relay)")
} else {
print("could not find relay when flushing: \(relayer.relay)")
}
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral) pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
} }
} }
-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) + "..."
}
}
+5 -3
View File
@@ -36,7 +36,8 @@ struct WalletConnectURL: Equatable {
} }
init?(str: String) { init?(str: String) {
guard let url = URL(string: str), url.scheme == "nostrwalletconnect", guard let url = URL(string: str),
url.scheme == "nostrwalletconnect" || url.scheme == "nostr+walletconnect",
let pk = url.host, pk.utf8.count == 64, let pk = url.host, pk.utf8.count == 64,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let items = components.queryItems, let items = components.queryItems,
@@ -174,12 +175,12 @@ func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypai
} }
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) { func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue]) var filter = NostrFilter(kinds: [.nwc_response])
filter.authors = [url.pubkey] filter.authors = [url.pubkey]
filter.limit = 0 filter.limit = 0
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc") let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
pool.send(.subscribe(sub), to: [url.relay.id]) pool.send(.subscribe(sub), to: [url.relay.id], skip_ephemeral: false)
} }
@discardableResult @discardableResult
@@ -232,6 +233,7 @@ func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL,
return return
} }
print("damus-donation donating...")
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil) nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
} }
+16 -8
View File
@@ -43,7 +43,7 @@ struct EventActionBar: View {
HStack { HStack {
if damus_state.keypair.privkey != nil { if damus_state.keypair.privkey != nil {
HStack(spacing: 4) { HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) { EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.compose, PostAction.replying_to(event)) notify(.compose, PostAction.replying_to(event))
} }
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button")) .accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
@@ -55,7 +55,7 @@ struct EventActionBar: View {
Spacer() Spacer()
HStack(spacing: 4) { HStack(spacing: 4) {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) { EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
if bar.boosted { if bar.boosted {
notify(.delete, bar.our_boost) notify(.delete, bar.our_boost)
} else { } else {
@@ -88,11 +88,11 @@ 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: "square.and.arrow.up", 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 post"))
@@ -161,9 +161,12 @@ struct EventActionBar: View {
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View { func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
Button(action: action) { Button(action: action) {
Image(systemName: img) Image(img)
.resizable()
.foregroundColor(col == nil ? Color.gray : col!) .foregroundColor(col == nil ? Color.gray : col!)
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
} }
} }
@@ -188,11 +191,16 @@ struct LikeButton: View {
}) { }) {
if liked { if liked {
LINEAR_GRADIENT LINEAR_GRADIENT
.mask(Image("shaka-full") .mask(Image("shaka.fill")
.resizable() .resizable()
).frame(width: 14, height: 14) .aspectRatio(contentMode: .fit)
)
.frame(width: 20, height: 20)
} else { } else {
Image("shaka-line") Image("shaka")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
+1 -1
View File
@@ -28,7 +28,7 @@ struct RepostAction: View {
damus_state.postbox.send(boost) damus_state.postbox.send(boost)
} label: { } label: {
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), systemImage: "arrow.2.squarepath") Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
} }
+2 -2
View File
@@ -41,7 +41,7 @@ struct ShareAction: View {
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id) UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
} }
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark" let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.") let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
ShareActionButton(img: bookmarkImg, text: bookmarkTxt) { ShareActionButton(img: bookmarkImg, text: bookmarkTxt) {
dismiss() dismiss()
@@ -54,7 +54,7 @@ struct ShareAction: View {
NotificationCenter.default.post(name: .broadcast_event, object: event) NotificationCenter.default.post(name: .broadcast_event, object: event)
} }
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) { ShareActionButton(img: "upload", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
show_share = true show_share = true
dismiss() dismiss()
} }
@@ -27,7 +27,7 @@ struct ShareActionButton: View {
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
VStack() { VStack() {
Image(systemName: img) Image(img)
.foregroundColor(col) .foregroundColor(col)
.font(.system(size: 23, weight: .bold)) .font(.system(size: 23, weight: .bold))
.overlay { .overlay {
@@ -48,6 +48,6 @@ struct ShareActionButton: View {
struct ShareActionButton_Previews: PreviewProvider { struct ShareActionButton_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ShareActionButton(img: "figure.flexibility", text: "Stretch", action: {}) ShareActionButton(img: "link", text: "Stretch", action: {})
} }
} }
+2 -2
View File
@@ -19,7 +19,7 @@ struct AddRelayView: View {
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
Label("", systemImage: "xmark.circle.fill") Label("", image: "close-circle")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.padding(.trailing, -25.0) .padding(.trailing, -25.0)
.opacity((relay == "") ? 0.0 : 1.0) .opacity((relay == "") ? 0.0 : 1.0)
@@ -28,7 +28,7 @@ struct AddRelayView: View {
} }
} }
Label("", systemImage: "doc.on.clipboard") Label("", image: "copy2")
.padding(.leading, -10) .padding(.leading, -10)
.onTapGesture { .onTapGesture {
if let pastedrelay = UIPasteboard.general.string { if let pastedrelay = UIPasteboard.general.string {
+1 -1
View File
@@ -29,7 +29,7 @@ struct BookmarksView: View {
Group { Group {
if bookmarks.isEmpty { if bookmarks.isEmpty {
VStack { VStack {
Image(systemName: "bookmark") Image("bookmark")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 32.0, height: 32.0) .frame(width: 32.0, height: 32.0)
+4 -4
View File
@@ -21,13 +21,13 @@ struct FriendsButton: View {
}) { }) {
if filter == .friends { if filter == .friends {
LINEAR_GRADIENT LINEAR_GRADIENT
.mask(Image(systemName: "person.2.fill") .mask(Image("user-added")
.resizable() .resizable()
).frame(width: 30, height: 20) ).frame(width: 28, height: 28)
} else { } else {
Image(systemName: "person.2.fill") Image("user-added")
.resizable() .resizable()
.frame(width: 30, height: 20) .frame(width: 28, height: 28)
.foregroundColor(DamusColors.adaptableGrey) .foregroundColor(DamusColors.adaptableGrey)
} }
} }
+18 -9
View File
@@ -37,23 +37,27 @@ struct ConfigView: View {
Form { Form {
Section { Section {
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) { NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple) IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key", color: .purple)
} }
NavigationLink(destination: AppearanceSettingsView(settings: settings)) { NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red) IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "eye", color: .red)
}
NavigationLink(destination: SearchSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "magnifyingglass", color: .red)
} }
NavigationLink(destination: NotificationSettingsView(settings: settings)) { NavigationLink(destination: NotificationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "bell.fill", color: .blue) IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
} }
NavigationLink(destination: ZapSettingsView(settings: settings)) { NavigationLink(destination: ZapSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange) IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
} }
NavigationLink(destination: TranslationSettingsView(settings: settings)) { NavigationLink(destination: TranslationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe.americas.fill", color: .green) IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
} }
} }
@@ -66,19 +70,24 @@ struct ConfigView: View {
confirm_logout = true confirm_logout = true
} }
}, label: { }, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit") Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), image: "logout")
.foregroundColor(textColor()) .foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
}) })
} }
if state.is_privkey_user { if state.is_privkey_user {
Section(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user")) { Section(header: Text(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user"))) {
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) { Button(action: {
delete_account_warning = true delete_account_warning = true
}, label: {
Label(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), image: "delete")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
})
} }
} }
}
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] { if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) { Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
+59 -51
View File
@@ -25,70 +25,63 @@ struct CreateAccountView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
DamusGradient() NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
EmptyView()
}
VStack { VStack {
Text("Create Account") VStack(alignment: .center) {
.font(.title.bold())
.foregroundColor(.white)
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
HStack(alignment: .top) { Text(NSLocalizedString("Public Key", comment: "Label to indicate the public key of the account."))
VStack { .bold()
Text(verbatim: " ") .padding()
.foregroundColor(.white)
}
VStack {
SignupForm {
FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry."))
HStack(spacing: 0.0) {
Text(verbatim: "@")
.foregroundColor(.white)
.padding(.leading, -25.0)
FormTextInput(NSLocalizedString("satoshi", comment: "Example username of Bitcoin creator(s), Satoshi Nakamoto."), text: $account.nick_name)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
FormLabel(NSLocalizedString("Display Name", comment: "Label to prompt display name entry."), optional: true)
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
.textInputAutocapitalization(.words)
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
FormLabel(NSLocalizedString("Account ID", comment: "Label to indicate the public ID of the account."))
.onTapGesture { .onTapGesture {
regen_key() regen_key()
} }
KeyText($account.pubkey) KeyText($account.pubkey)
.padding(.horizontal, 20)
.onTapGesture { .onTapGesture {
regen_key() regen_key()
} }
} }
} .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, alignment: .center)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1)
} }
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) { SignupForm {
EmptyView() FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true)
} FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
.textInputAutocapitalization(.words)
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) { FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
}
.padding(.top, 10)
Button(action: {
self.is_done = true self.is_done = true
}) {
HStack {
Text("Create account now", comment: "Button to create account.")
.fontWeight(.semibold)
} }
.padding() .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.disabled(profileUploadViewModel.isLoading) .disabled(profileUploadViewModel.isLoading)
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1) .opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
} .padding(.top, 20)
.padding(.leading, 14.0)
.padding(.trailing, 20.0)
LoginPrompt()
}
.padding()
} }
.dismissKeyboardOnTap() .dismissKeyboardOnTap()
.navigationTitle("Create account")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav()) .navigationBarItems(leading: BackNav())
@@ -99,11 +92,26 @@ struct CreateAccountView: View {
} }
} }
struct LoginPrompt: View {
@Environment(\.dismiss) var dismiss
var body: some View {
HStack {
Text("Already on nostr?", comment: "Ask the user if they already have an account on nostr")
.foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
self.dismiss()
}
Spacer()
}
}
}
struct BackNav: View { struct BackNav: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
Image(systemName: "chevron.backward") Image("chevron-left")
.foregroundColor(.white) .foregroundColor(.white)
.onTapGesture { .onTapGesture {
self.dismiss() self.dismiss()
@@ -136,20 +144,21 @@ func KeyText(_ text: Binding<String>) -> some View {
let bechkey = bech32_encode(hrp: PUBKEY_HRP, decoded) let bechkey = bech32_encode(hrp: PUBKEY_HRP, decoded)
return Text(bechkey) return Text(bechkey)
.textSelection(.enabled) .textSelection(.enabled)
.multilineTextAlignment(.center)
.font(.callout.monospaced()) .font(.callout.monospaced())
.foregroundColor(.white) .foregroundStyle(DamusLogoGradient.gradient)
} }
func FormTextInput(_ title: String, text: Binding<String>) -> some View { func FormTextInput(_ title: String, text: Binding<String>) -> some View {
return TextField("", text: text) return TextField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) { .placeholder(when: text.wrappedValue.isEmpty) {
Text(title).foregroundColor(.white.opacity(0.4)) Text(title).foregroundColor(.gray.opacity(0.5))
} }
.padding() .padding(15)
.background { .background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.2) RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.5), lineWidth: 1)
} }
.foregroundColor(.white)
.font(.body.bold()) .font(.body.bold())
} }
@@ -157,11 +166,10 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
return HStack { return HStack {
Text(title) Text(title)
.bold() .bold()
.foregroundColor(.white)
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(.white.opacity(0.5)) .foregroundColor(DamusColors.mediumGrey)
} }
} }
} }
+1 -1
View File
@@ -115,7 +115,7 @@ struct DMChatView: View, KeyboardReadable {
} }
} }
) { ) {
Label("", systemImage: "arrow.right.circle") Label("", image: "send")
.font(.title) .font(.title)
} }
} }
+68 -40
View File
@@ -7,28 +7,14 @@
import SwiftUI import SwiftUI
struct EULAView: View { let eula = """
var state: SetupState? **End User License Agreement**
@Environment(\.dismiss) var dismiss
@State var accepted = false
var body: some View { **Introduction**
ZStack {
DamusGradient()
ScrollView {
Text("EULA", comment: "Label indicating that the below text is the EULA, an acronym for End User License Agreement.")
.font(.title.bold())
.foregroundColor(.white)
Text(Markdown.parse(content: """
End User License Agreement
## Introduction
This End User License Agreement ("EULA") is a legal agreement between you and Damus Nostr Inc. for the use of our mobile application Damus. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA. This End User License Agreement ("EULA") is a legal agreement between you and Damus Nostr Inc. for the use of our mobile application Damus. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA.
## Prohibited Content and Conduct **Prohibited Content and Conduct**
You agree not to use our application to create, upload, post, send, or store any content that: You agree not to use our application to create, upload, post, send, or store any content that:
@@ -40,59 +26,101 @@ You agree not to use our application to create, upload, post, send, or store any
* Is intended to harass or bully others * Is intended to harass or bully others
* Is intended to impersonate others * Is intended to impersonate others
## You also agree not to engage in any conduct that: **You also agree not to engage in any conduct that:**
* Harasses or bullies others * Harasses or bullies others
* Impersonates others * Impersonates others
* Is intended to intimidate or threaten others * Is intended to intimidate or threaten others
* Is intended to promote or incite violence * Is intended to promote or incite violence
## Consequences of Violation **Consequences of Violation**
Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application. Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application.
## Disclaimer of Warranties and Limitation of Liability **Disclaimer of Warranties and Limitation of Liability**
Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Damus Nostr Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application. Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Damus Nostr Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application.
## Changes to EULA **Changes to EULA**
We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes. We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes.
## Contact Information **Contact Information**
If you have any questions about this EULA, please contact us at damus@jb55.com If you have any questions about this EULA, please contact us at damus@jb55.com
## Acceptance of Terms **Acceptance of Terms**
By using our Application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our Application. By using our Application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our Application.
""")) """
struct EULAView: View {
@State private var login = false
@State var accepted = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
ScrollView {
NavigationLink(destination: LoginView(accepted: $accepted), isActive: $login) {
EmptyView()
}
Text(Markdown.parse(content: eula))
.padding() .padding()
}
.padding(EdgeInsets(top: 20, leading: 10, bottom: 50, trailing: 10))
if state == .create_account { VStack {
NavigationLink(destination: CreateAccountView(), isActive: $accepted) { Spacer()
EmptyView()
}
} else {
NavigationLink(destination: LoginView(), isActive: $accepted) {
EmptyView()
}
}
DamusWhiteButton(NSLocalizedString("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")) {
accepted = true
}
DamusWhiteButton(NSLocalizedString("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")) { HStack {
Spacer()
Button(action: {
dismiss() dismiss()
}) {
HStack {
Text("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")
.fontWeight(.semibold)
} }
} .frame(minWidth: 75, maxHeight: 12, alignment: .center)
.padding() .padding()
.foregroundColor(Color.white)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(DamusColors.darkGrey, strokeBorder: DamusColors.mediumGrey, lineWidth: 1)
} }
}
Button(action: {
accepted = true
login.toggle()
}) {
HStack {
Text("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 75, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
}
.padding(.trailing, 30)
}
}
.background(
Image("eula-bg")
.resizable()
.blur(radius: 70)
.ignoresSafeArea(),
alignment: .top
)
.navigationTitle("EULA")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav()) .navigationBarItems(leading: BackNav())
.foregroundColor(.white)
} }
} }
@@ -10,7 +10,7 @@ import SwiftUI
struct EmptyTimelineView: View { struct EmptyTimelineView: View {
var body: some View { var body: some View {
VStack { VStack {
Image(systemName: "tray.fill") Image("question")
.font(.system(size: 35)) .font(.system(size: 35))
.padding() .padding()
Text("Nothing to see here. Check back later!", comment: "Indicates that there are no notes in the timeline to view.") Text("Nothing to see here. Check back later!", comment: "Indicates that there are no notes in the timeline to view.")
@@ -17,7 +17,7 @@ import SwiftUI
struct EmptyUserSearchView: View { struct EmptyUserSearchView: View {
var body: some View { var body: some View {
VStack { VStack {
Image(systemName: "person.fill.questionmark") Image("question")
.font(.system(size: 35)) .font(.system(size: 35))
.padding() .padding()
Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.") Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.")
+6 -1
View File
@@ -12,6 +12,7 @@ enum EventViewKind {
case small case small
case normal case normal
case selected case selected
case subheadline
} }
struct EventView: View { struct EventView: View {
@@ -73,7 +74,7 @@ extension View {
Button { Button {
UIPasteboard.general.string = bech32_pubkey UIPasteboard.general.string = bech32_pubkey
} label: { } label: {
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), systemImage: "doc.on.doc") Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2")
} }
} }
} }
@@ -106,6 +107,8 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
return .body return .body
case .selected: case .selected:
return .custom("selected", size: 21.0) return .custom("selected", size: 21.0)
case .subheadline:
return .subheadline
} }
} }
@@ -117,6 +120,8 @@ func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
return .preferredFont(forTextStyle: .body) return .preferredFont(forTextStyle: .body)
case .selected: case .selected:
return .preferredFont(forTextStyle: .title2) return .preferredFont(forTextStyle: .title2)
case .subheadline:
return .preferredFont(forTextStyle: .subheadline)
} }
} }
+1 -1
View File
@@ -61,7 +61,7 @@ struct BuilderEventView: View {
subscribe(filters: [ subscribe(filters: [
NostrFilter(ids: [self.event_id], limit: 1), NostrFilter(ids: [self.event_id], limit: 1),
NostrFilter( NostrFilter(
kinds: [NostrKind.zap.rawValue], kinds: [.zap],
referenced_ids: [self.event_id] referenced_ids: [self.event_id]
) )
]) ])
+12 -12
View File
@@ -60,25 +60,25 @@ struct MenuItems: View {
Button { Button {
UIPasteboard.general.string = event.get_content(keypair.privkey) UIPasteboard.general.string = event.get_content(keypair.privkey)
} label: { } label: {
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc") Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2")
} }
Button { Button {
UIPasteboard.general.string = bech32_pubkey(target_pubkey) UIPasteboard.general.string = bech32_pubkey(target_pubkey)
} label: { } label: {
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person") Label(NSLocalizedString("Copy user public key", comment: "Context menu option for copying the ID of the user who created the note."), image: "user")
} }
Button { Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
} label: { } label: {
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text") Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
} }
Button { Button {
UIPasteboard.general.string = event_to_json(ev: event) UIPasteboard.general.string = event_to_json(ev: event)
} label: { } label: {
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square") Label(NSLocalizedString("Copy note JSON", comment: "Context menu option for copying the JSON text from the note."), image: "code.on.square")
} }
Button { Button {
@@ -86,9 +86,9 @@ struct MenuItems: View {
isBookmarked = self.bookmarks.isBookmarked(event) isBookmarked = self.bookmarks.isBookmarked(event)
} label: { } label: {
let imageName = isBookmarked ? "bookmark.fill" : "bookmark" let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.") let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.")
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.") let addBookmarkString = NSLocalizedString("Add bookmark", comment: "Context menu option for adding a note bookmark.")
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName) Label(isBookmarked ? removeBookmarkString : addBookmarkString, image: imageName)
} }
if event.known_kind != .dm { if event.known_kind != .dm {
@@ -97,17 +97,17 @@ struct MenuItems: View {
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey) let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
isMutedThread = muted isMutedThread = muted
} label: { } label: {
let imageName = isMutedThread ? "speaker" : "speaker.slash" let imageName = isMutedThread ? "mute" : "mute"
let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.") let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.")
let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.") let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.")
Label(isMutedThread ? unmuteThreadString : muteThreadString, systemImage: imageName) Label(isMutedThread ? unmuteThreadString : muteThreadString, image: imageName)
} }
} }
Button { Button {
NotificationCenter.default.post(name: .broadcast_event, object: event) NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: { } label: {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe") Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), image: "globe")
} }
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile. // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
@@ -116,13 +116,13 @@ struct MenuItems: View {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id)) let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
notify(.report, target) notify(.report, target)
} label: { } label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble") Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand")
} }
Button(role: .destructive) { Button(role: .destructive) {
notify(.mute, target_pubkey) notify(.mute, target_pubkey)
} label: { } label: {
Label(NSLocalizedString("Mute User", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon") Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
} }
} }
} }
+2
View File
@@ -15,6 +15,8 @@ func eventview_pfp_size(_ size: EventViewKind) -> CGFloat {
return PFP_SIZE return PFP_SIZE
case .selected: case .selected:
return PFP_SIZE return PFP_SIZE
case .subheadline:
return PFP_SIZE * 0.5
} }
} }
+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 -1
View File
@@ -19,7 +19,7 @@ struct ZapEvent: View {
.padding([.top], 2) .padding([.top], 2)
if zap.is_private { if zap.is_private {
Image(systemName: "lock.fill") Image("lock")
.foregroundColor(DamusColors.green) .foregroundColor(DamusColors.green)
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap.")) .help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
} }
+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)
} }
} }
@@ -19,24 +19,24 @@ struct ImageContextMenuModifier: ViewModifier {
Button { Button {
UIPasteboard.general.url = url UIPasteboard.general.url = url
} label: { } label: {
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc") Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), image: "copy2")
} }
if let someImage = image { if let someImage = image {
Button { Button {
UIPasteboard.general.image = someImage UIPasteboard.general.image = someImage
} label: { } label: {
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle") Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), image: "copy2.fill")
} }
Button { Button {
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil) UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
} label: { } label: {
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down") Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), image: "download")
} }
} }
Button { Button {
showShareSheet = true showShareSheet = true
} label: { } label: {
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up") Label(NSLocalizedString("Share", comment: "Button to share an image."), image: "upload")
} }
} }
} }
+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)
} }
} }
+1 -1
View File
@@ -49,7 +49,7 @@ struct NavDismissBarView: View {
Button(action: { Button(action: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
}, label: { }, label: {
Image(systemName: "xmark") Image("close")
.frame(width: 33, height: 33) .frame(width: 33, height: 33)
.background(.regularMaterial) .background(.regularMaterial)
.clipShape(Circle()) .clipShape(Circle())
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="gradient" translatesAutoresizingMaskIntoConstraints="NO" id="zoF-av-bOb">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="damus-home" translatesAutoresizingMaskIntoConstraints="NO" id="LOu-EK-R9r">
<rect key="frame" x="153.66666666666666" y="383" width="86" height="86"/>
<constraints>
<constraint firstAttribute="height" constant="86" id="KmA-28-Ngq"/>
<constraint firstAttribute="width" constant="86" id="ShD-nJ-gt9"/>
</constraints>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemPurpleColor"/>
<constraints>
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="Y10-Wq-VOp"/>
<constraint firstItem="zoF-av-bOb" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="Y5l-Ax-ViU"/>
<constraint firstItem="zoF-av-bOb" firstAttribute="leading" secondItem="5EZ-qb-Rvc" secondAttribute="leading" id="bvq-6J-kYc"/>
<constraint firstAttribute="bottom" secondItem="zoF-av-bOb" secondAttribute="bottom" id="dfj-BJ-nxB"/>
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerX" secondItem="5EZ-qb-Rvc" secondAttribute="centerX" id="mtD-6Q-d3P"/>
<constraint firstAttribute="right" secondItem="zoF-av-bOb" secondAttribute="right" id="xQW-SS-8nb"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-106.10687022900763" y="-29.577464788732396"/>
</scene>
</scenes>
<resources>
<image name="damus-home" width="43.333332061767578" height="43.333332061767578"/>
<image name="gradient" width="1125" height="2400"/>
<systemColor name="systemPurpleColor">
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+108 -52
View File
@@ -33,11 +33,14 @@ enum ParsedKey {
} }
struct LoginView: View { struct LoginView: View {
@State private var create_account = false
@State var key: String = "" @State var key: String = ""
@State var is_pubkey: Bool = false @State var is_pubkey: Bool = false
@State var error: String? = nil @State var error: String? = nil
@State private var credential_handler = CredentialHandler() @State private var credential_handler = CredentialHandler()
@Binding var accepted: Bool
func get_error(parsed_key: ParsedKey?) -> String? { func get_error(parsed_key: ParsedKey?) -> String? {
if self.error != nil { if self.error != nil {
return self.error return self.error
@@ -52,27 +55,22 @@ struct LoginView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
DamusGradient() if accepted {
NavigationLink(destination: CreateAccountView(), isActive: $create_account) {
EmptyView()
}
}
VStack { VStack {
Text("Login", comment: "Title of view to log into an account.") SignInHeader()
.foregroundColor(.white) .padding(.top, 100)
.font(.title)
.padding()
Text("Enter your account key to login:", comment: "Prompt for user to enter an account key to login.") SignInEntry(key: $key)
.foregroundColor(.white)
.padding()
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: $key)
let parsed = parse_key(key) let parsed = parse_key(key)
if parsed?.is_hex ?? false { if parsed?.is_hex ?? false {
Text("This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.", comment: "Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key.") // convert to bech32 here
.font(.subheadline.bold())
.foregroundColor(.white)
PubkeySwitch(isOn: $is_pubkey)
.padding()
} }
if let error = get_error(parsed_key: parsed) { if let error = get_error(parsed_key: parsed) {
@@ -83,14 +81,13 @@ 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 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.")
.foregroundColor(.white) .foregroundColor(Color.orange)
.padding() .bold()
} }
Spacer()
if let p = parsed { if let p = parsed {
DamusWhiteButton(NSLocalizedString("Login", comment: "Button to log into account.")) {
Button(action: {
Task { Task {
do { do {
try await process_login(p, is_pubkey: is_pubkey) try await process_login(p, is_pubkey: is_pubkey)
@@ -98,11 +95,31 @@ struct LoginView: View {
self.error = error.localizedDescription self.error = error.localizedDescription
} }
} }
}) {
HStack {
Text("Login", comment: "Button to log into account.")
.fontWeight(.semibold)
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
} }
.buttonStyle(GradientButtonStyle())
.padding(.top, 10)
}
CreateAccountPrompt(create_account: $create_account)
.padding(.top, 10)
Spacer()
} }
.padding() .padding()
} }
.background(
Image("login-header")
.resizable()
.frame(maxWidth: .infinity, maxHeight: 350, alignment: .center)
.ignoresSafeArea(),
alignment: .top
)
.onAppear { .onAppear {
credential_handler.check_credentials() credential_handler.check_credentials()
} }
@@ -111,18 +128,6 @@ struct LoginView: View {
} }
} }
struct PubkeySwitch: View {
@Binding var isOn: Bool
var body: some View {
HStack {
Toggle(isOn: $isOn) {
Text("Public Key?", comment: "Prompt to ask user if the key they entered is a public key.")
.foregroundColor(.white)
}
}
}
}
func parse_key(_ thekey: String) -> ParsedKey? { func parse_key(_ thekey: String) -> ParsedKey? {
var key = thekey var key = thekey
if key.count > 0 && key.first! == "@" { if key.count > 0 && key.first! == "@" {
@@ -270,39 +275,90 @@ struct KeyInput: View {
} }
var body: some View { var body: some View {
ZStack(alignment: .leading) { HStack {
TextField("", text: key) Image(systemName: "doc.on.clipboard")
.placeholder(when: key.wrappedValue.isEmpty) { .foregroundColor(.gray)
Text(title).foregroundColor(.white.opacity(0.6))
}
.padding()
.padding(.leading, 20)
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
}
.autocapitalization(.none)
.foregroundColor(.white)
.font(.body.monospaced())
.textContentType(.password)
Label("", systemImage: "doc.on.clipboard")
.padding(.leading, 10)
.onTapGesture { .onTapGesture {
if let pastedkey = UIPasteboard.general.string { if let pastedkey = UIPasteboard.general.string {
self.key.wrappedValue = pastedkey self.key.wrappedValue = pastedkey
} }
} }
TextField("", text: key)
.placeholder(when: key.wrappedValue.isEmpty) {
Text(title).foregroundColor(.white.opacity(0.6))
}
.padding(10)
.autocapitalization(.none)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.font(.body.monospaced())
.textContentType(.password)
}
.padding(.horizontal, 10)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(.gray, lineWidth: 1)
}
}
}
struct SignInHeader: View {
var body: some View {
VStack {
Image("logo-nobg")
.resizable()
.frame(width: 56, height: 56, alignment: .center)
.shadow(color: DamusColors.purple, radius: 2)
.padding(.bottom)
Text("Sign in", comment: "Title of view to log into an account.")
.font(.system(size: 32, weight: .bold))
.padding(.bottom, 5)
Text("Welcome to the social network you control", comment: "Welcome text")
.foregroundColor(Color("DamusMediumGrey"))
}
}
}
struct SignInEntry: View {
let key: Binding<String>
var body: some View {
VStack(alignment: .leading) {
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
.fontWeight(.medium)
.padding(.top, 30)
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: key)
}
}
}
struct CreateAccountPrompt: View {
@Binding var create_account: Bool
var body: some View {
HStack {
Text("New to nostr?", comment: "Ask the user if they are new to nostr")
.foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
create_account.toggle()
}
Spacer()
} }
} }
} }
struct LoginView_Previews: PreviewProvider { struct LoginView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" // let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955"
let bech32_pubkey = "KeyInput" let bech32_pubkey = "KeyInput"
Group { Group {
LoginView(key: pubkey) LoginView(key: pubkey, accepted: .constant(true))
LoginView(key: bech32_pubkey) LoginView(key: bech32_pubkey, accepted: .constant(true))
} }
} }
} }
+6 -6
View File
@@ -55,11 +55,11 @@ struct TabButton: View {
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) new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
}) { }) {
Label("", systemImage: selected == timeline ? "\(img).fill" : img) Image(selected != timeline ? img : "\(img).fill")
.contentShape(Rectangle()) .contentShape(Rectangle())
.frame(maxWidth: .infinity, minHeight: 30.0) .frame(maxWidth: .infinity, minHeight: 30.0)
} }
.foregroundColor(selected != timeline ? .gray : .primary) .foregroundColor(.primary)
} }
} }
@@ -75,10 +75,10 @@ struct TabBar: View {
VStack { VStack {
Divider() Divider()
HStack { HStack {
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1") TabButton(timeline: .home, img: "home", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2") TabButton(timeline: .dms, img: "messages", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3") TabButton(timeline: .search, img: "search", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4") TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
} }
} }
} }
+1 -1
View File
@@ -29,7 +29,7 @@ struct MutelistView: View {
damus_state.postbox.send(new_ev) damus_state.postbox.send(new_ev)
users = get_mutelist_users(new_ev) users = get_mutelist_users(new_ev)
} label: { } label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), systemImage: "trash") Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
} }
.tint(.red) .tint(.red)
} }
+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 url
}
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? {
@@ -61,7 +61,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 {
@@ -168,7 +168,7 @@ struct EventGroupView: View {
func ZapIcon(_ zapgrp: ZapGroup) -> some View { func ZapIcon(_ zapgrp: ZapGroup) -> some View {
let fmt = format_msats_abbrev(zapgrp.msat_total) let fmt = format_msats_abbrev(zapgrp.msat_total)
return VStack(alignment: .center) { return VStack(alignment: .center) {
Image(systemName: "bolt.fill") Image("zap.fill")
.foregroundColor(.orange) .foregroundColor(.orange)
Text(verbatim: fmt) Text(verbatim: fmt)
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
@@ -179,13 +179,15 @@ struct EventGroupView: View {
Group { Group {
switch group { switch group {
case .repost: case .repost:
Image(systemName: "arrow.2.squarepath") Image("repost")
.foregroundColor(DamusColors.green) .foregroundColor(DamusColors.green)
case .reaction: case .reaction:
LINEAR_GRADIENT LINEAR_GRADIENT
.mask(Image("shaka-full") .mask(Image("shaka.fill")
.resizable() .resizable()
).frame(width: 24, height: 24) .aspectRatio(contentMode: .fit)
)
.frame(width: 20, height: 20)
case .profile_zap(let zapgrp): case .profile_zap(let zapgrp):
ZapIcon(zapgrp) ZapIcon(zapgrp)
case .zap(let zapgrp): case .zap(let zapgrp):
@@ -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")
+4 -2
View File
@@ -57,7 +57,9 @@ struct ParticipantsView: View {
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 = profile?.about {
Text(FollowUserView.markdown.process(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) .lineLimit(3)
.font(.footnote) .font(.footnote)
} }
@@ -65,7 +67,7 @@ struct ParticipantsView: View {
Spacer() Spacer()
Image(systemName: "checkmark.circle.fill") Image("check-circle.fill")
.font(.system(size: 30)) .font(.system(size: 30))
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray) .foregroundColor(references.contains(participant) ? DamusColors.purple : .gray)
} }
+1 -1
View File
@@ -26,7 +26,7 @@ func PostButton(action: @escaping () -> ()) -> some View {
radius: 3, radius: 3,
x: 3, x: 3,
y: 3) y: 3)
Image(systemName: "plus") Image("plus")
.font(.system(.title2)) .font(.system(.title2))
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
+14 -6
View File
@@ -15,6 +15,10 @@ enum NostrPostResult {
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 post here...", comment: "Text box prompt to ask user to type their post.")
class TagModel: ObservableObject {
var diff = 0
}
enum PostAction { enum PostAction {
case replying_to(NostrEvent) case replying_to(NostrEvent)
case quoting(NostrEvent) case quoting(NostrEvent)
@@ -45,10 +49,12 @@ struct PostView: View {
@State var references: [ReferencedId] = [] @State var references: [ReferencedId] = []
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil) @State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
@State var newCursorIndex: Int? @State var newCursorIndex: Int?
@State var postTextViewCanScroll: Bool = true
@State var mediaToUpload: MediaUpload? = nil @State var mediaToUpload: MediaUpload? = nil
@StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var image_upload: ImageUploadModel = ImageUploadModel()
@StateObject var tagModel: TagModel = TagModel()
let action: PostAction let action: PostAction
let damus_state: DamusState let damus_state: DamusState
@@ -108,7 +114,7 @@ struct PostView: View {
Button(action: { Button(action: {
attach_media = true attach_media = true
}, label: { }, label: {
Image(systemName: "photo") Image("images")
.padding(6) .padding(6)
}) })
} }
@@ -117,7 +123,7 @@ struct PostView: View {
Button(action: { Button(action: {
attach_camera = true attach_camera = true
}, label: { }, label: {
Image(systemName: "camera") Image("camera")
.padding(6) .padding(6)
}) })
} }
@@ -203,10 +209,11 @@ struct PostView: View {
var TextEntry: some View { var TextEntry: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post, 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)
.focused($focus) .focused($focus)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.onChange(of: post) { p in .onChange(of: post) { p in
@@ -335,8 +342,9 @@ struct PostView: View {
// This if-block observes @ for tagging // This if-block observes @ for tagging
if let searching { if let searching {
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, post: $post) UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
.environmentObject(tagModel)
} else { } else {
Divider() Divider()
VStack(alignment: .leading) { VStack(alignment: .leading) {
@@ -463,11 +471,11 @@ struct PVImageCarouselView: View {
Button(action: { Button(action: {
UIPasteboard.general.string = uploadedURL.absoluteString UIPasteboard.general.string = uploadedURL.absoluteString
}) { }) {
Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), systemImage: "doc.on.doc") Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), image: "copy")
} }
} }
} }
Image(systemName: "xmark.circle.fill") Image("close-circle")
.foregroundColor(.white) .foregroundColor(.white)
.padding(20) .padding(20)
.shadow(radius: 5) .shadow(radius: 5)
+16 -3
View File
@@ -22,8 +22,10 @@ struct UserSearch: View {
let search: String let search: String
@Binding var focusWordAttributes: (String?, NSRange?) @Binding var focusWordAttributes: (String?, NSRange?)
@Binding var newCursorIndex: Int? @Binding var newCursorIndex: Int?
@Binding var postTextViewCanScroll: Bool
@Binding var post: NSMutableAttributedString @Binding var post: NSMutableAttributedString
@EnvironmentObject var tagModel: TagModel
var users: [SearchedUser] { var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else { guard let contacts = damus_state.contacts.event else {
@@ -47,13 +49,16 @@ struct UserSearch: View {
} }
let mutableString = NSMutableAttributedString(attributedString: post) let mutableString = NSMutableAttributedString(attributedString: post)
mutableString.replaceCharacters(in: wordRange, with: tagAttributedString) mutableString.replaceCharacters(in: wordRange, with: tagAttributedString)
///adjust cursor position appropriately: ('diff' used in TextViewWrapper / updateUIView after below update of 'post')
tagModel.diff = tagAttributedString.length - wordRange.length
post = mutableString post = mutableString
focusWordAttributes = (nil, nil) focusWordAttributes = (nil, nil)
newCursorIndex = wordRange.location + tagAttributedString.string.count newCursorIndex = wordRange.location + tagAttributedString.string.count
} }
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,
@@ -92,7 +97,14 @@ struct UserSearch: View {
.padding() .padding()
} }
} }
.onAppear() {
postTextViewCanScroll = false
} }
.onDisappear() {
postTextViewCanScroll = true
}
}
} }
struct UserSearch_Previews: PreviewProvider { struct UserSearch_Previews: PreviewProvider {
@@ -100,9 +112,10 @@ struct UserSearch_Previews: PreviewProvider {
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
@State static var word: (String?, NSRange?) = (nil, nil) @State static var word: (String?, NSRange?) = (nil, nil)
@State static var newCursorIndex: Int? @State static var newCursorIndex: Int?
@State static var postTextViewCanScroll: Bool = false
static var previews: some View { static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, post: $post) UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
} }
} }
@@ -140,7 +153,7 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search
} }
// search profile cache as well // search profile cache as well
for tup in profiles.profiles.enumerated() { for tup in profiles.enumerated() {
let pk = tup.element.key let pk = tup.element.key
let prof = tup.element.value.profile let prof = tup.element.value.profile
@@ -39,7 +39,7 @@ struct EditProfilePictureControl: View {
if viewModel.isLoading { if viewModel.isLoading {
ProgressView() ProgressView()
} else { } else {
Image(systemName: "camera") Image("camera")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 25, height: 25) .frame(width: 25, height: 25)
@@ -15,6 +15,7 @@ struct EventProfileName: View {
@State var display_name: DisplayName? @State var display_name: DisplayName?
@State var nip05: NIP05? @State var nip05: NIP05?
@State var donation: Int?
let size: EventViewKind let size: EventViewKind
@@ -23,6 +24,7 @@ struct EventProfileName: View {
self.pubkey = pubkey self.pubkey = pubkey
self.profile = profile self.profile = profile
self.size = size self.size = size
self._donation = State(wrappedValue: profile?.damus_donation)
} }
var friend_type: FriendType? { var friend_type: FriendType? {
@@ -45,6 +47,15 @@ struct EventProfileName: View {
return profile.reactions == false return profile.reactions == false
} }
var supporter: Int? {
guard let donation, donation > 0
else {
return nil
}
return donation
}
var body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
switch current_display_name { switch current_display_name {
@@ -73,6 +84,10 @@ struct EventProfileName: View {
Image("zap-hashtag") Image("zap-hashtag")
.frame(width: 14, height: 14) .frame(width: 14, height: 14)
} }
if let supporter {
SupporterBadge(percent: supporter)
}
} }
.onReceive(handle_notify(.profile_updated)) { notif in .onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate let update = notif.object as! ProfileUpdate
@@ -81,6 +96,7 @@ struct EventProfileName: View {
} }
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey) nip05 = damus_state.profiles.is_validated(pubkey)
donation = update.profile.damus_donation
} }
} }
} }
+2 -1
View File
@@ -23,7 +23,8 @@ struct MaybeAnonPfpView: View {
var body: some View { var body: some View {
Group { Group {
if is_anon { if is_anon {
Image(systemName: "person.fill.questionmark") Image("question")
.resizable()
.font(.largeTitle) .font(.largeTitle)
.frame(width: size, height: size) .frame(width: size, height: size)
} else { } else {
+17 -1
View File
@@ -34,6 +34,7 @@ struct ProfileName: View {
@State var display_name: DisplayName? @State var display_name: DisplayName?
@State var nip05: NIP05? @State var nip05: NIP05?
@State var donation: Int?
init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) { init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) {
self.pubkey = pubkey self.pubkey = pubkey
@@ -64,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 {
@@ -75,6 +76,17 @@ struct ProfileName: View {
return profile.reactions == false return profile.reactions == false
} }
var supporter: Int? {
guard let profile,
let donation = profile.damus_donation,
donation > 0
else {
return nil
}
return donation
}
var body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(name_choice)") Text(verbatim: "\(prefix)\(name_choice)")
@@ -90,6 +102,9 @@ struct ProfileName: View {
Image("zap-hashtag") Image("zap-hashtag")
.frame(width: 14, height: 14) .frame(width: 14, height: 14)
} }
if let supporter {
SupporterBadge(percent: supporter)
}
} }
.onReceive(handle_notify(.profile_updated)) { notif in .onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate let update = notif.object as! ProfileUpdate
@@ -98,6 +113,7 @@ struct ProfileName: View {
} }
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey) nip05 = damus_state.profiles.is_validated(pubkey)
donation = profile?.damus_donation
} }
} }
} }

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