Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
bc358dd164
|
@@ -1,53 +1,3 @@
|
||||
## [1.5-5] - 2023-06-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove note zaps to fit apples appstore guidelines
|
||||
- Fix zap sheet popping (William Casarin)
|
||||
- Fix CustomizeZapView from randomly disappearing (William Casarin)
|
||||
- Fix "zapped your profile" strings to say "zapped you" (Terry Yiu)
|
||||
- Fix reconnect loop issues on iOS17 (William Casarin)
|
||||
- Fix some more thread jankiness (William Casarin)
|
||||
- Fix spelling of Nostr to use Titlecase instead of lowercase (Terry Yiu)
|
||||
- Rename all usages of the term Post as a noun to Note to conform to the Nostr spec (Terry Yiu)
|
||||
- Fix text cutoff on login with npub (gladiusKatana)
|
||||
- Fix hangs due to video player (William Casarin)
|
||||
|
||||
|
||||
[1.5-5]: https://github.com/damus-io/damus/releases/tag/v1.5-5
|
||||
|
||||
## [1.5-2] - 2023-05-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add new full-bleed video player (William Casarin)
|
||||
- Add ability to show multiple posts per user in Universe (Ben Weeks)
|
||||
- Custom iconography added for other areas of the app. (Ben Weeks)
|
||||
- Custom iconography for the left navigation. (Ben Weeks)
|
||||
- Custom iconography for the tab buttons. (Ben Weeks)
|
||||
- Added dots under image carousel (Ben Weeks)
|
||||
- Add profile caching (Bryan Montz)
|
||||
- Add mention parsing and fine-grained text selection on description in ProfileView (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesign phase 1 (text, icons)
|
||||
- Updated UI to use custom font (Ben Weeks)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix side menu bug in landscape (OlegAba)
|
||||
- Use "Follow me on nostr" text when looking at someone else's QR code (Ben Weeks)
|
||||
- Fix issue where cursor dissapears when typing long message (gladiusKatana)
|
||||
- Attempt fix for randomly broken animated gifs (William Casarin)
|
||||
- Fix cursor jumping when pressing return (gladius)
|
||||
- Fix side menu label size so that translations in longer languages fit without wrapping (Terry Yiu)
|
||||
- Fix reaction notification title to be consistent with ReactionView (Terry Yiu)
|
||||
- Fix nostr URL scheme to open properly even if there's already a different view open (Terry Yiu)
|
||||
- Fix crash related to preloading events (Bryan Montz)
|
||||
|
||||
|
||||
## v1.4.3 - 2023-05-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -94,33 +94,10 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
|
||||
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
|
||||
|
||||
### Mailing lists
|
||||
|
||||
We have a few mailing lists that anyone can join to get involved in damus development:
|
||||
|
||||
- [dev][dev-list] - development discussions
|
||||
- [patches][patches-list] - code submission and review
|
||||
- [product][product-list] - product discussions
|
||||
- [design][design-list] - design discussions
|
||||
|
||||
[dev-list]: https://damus.io/list/dev
|
||||
[patches-list]: https://damus.io/list/patches
|
||||
[product-list]: https://damus.io/list/product
|
||||
[design-list]: https://damus.io/list/design
|
||||
|
||||
### Code
|
||||
|
||||
[Email patches][git-send-email] to patches@damus.io are preferred, but I accept PRs on GitHub as well. Patches sent via email may include a bolt11 lightning invoice, choosing the price you think the patch is worth, and I will pay it once the patch is accepted and if I think the price isn't unreasonable. You can also send an any-amount invoice and I will pay what I think it's worth if you prefer not to choose. You can include the bolt11 in the commit body or email so that it can be paid once it is applied.
|
||||
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
|
||||
|
||||
Recommended settings when submitting code via email:
|
||||
|
||||
```
|
||||
$ git config sendemail.to "patches@damus.io"
|
||||
$ git config format.subjectPrefix "PATCH damus"
|
||||
$ git config --global sendemail.annotate yes
|
||||
$ git config format.signOff yes
|
||||
```
|
||||
|
||||
[git-send-email]: http://git-send-email.io
|
||||
|
||||
### Privacy
|
||||
|
||||
@@ -11,16 +11,13 @@
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.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 */; };
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
@@ -52,7 +49,6 @@
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.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 */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@@ -132,7 +128,6 @@
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
|
||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
|
||||
@@ -171,8 +166,6 @@
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */; };
|
||||
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; };
|
||||
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
|
||||
@@ -200,7 +193,6 @@
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
||||
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; };
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
|
||||
@@ -215,8 +207,6 @@
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.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 */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
@@ -244,7 +234,7 @@
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
|
||||
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */; };
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
|
||||
@@ -304,8 +294,15 @@
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||
E4AE2AC92A227E6000680283 /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC82A227E6000680283 /* LICENSE.txt */; };
|
||||
E4AE2ACA2A227E7800680283 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC62A227E4200680283 /* Inter-Regular.otf */; };
|
||||
E4AE2ACB2A227E7E00680283 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC52A227E3600680283 /* Inter-Bold.otf */; };
|
||||
E4AE2ACC2A227E8300680283 /* Inter-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AC72A227E5400680283 /* Inter-Light.otf */; };
|
||||
E4AE2ACE2A2286D000680283 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4AE2ACD2A2286D000680283 /* FontManager.swift */; };
|
||||
E4AE2AD32A228CB400680283 /* Inter-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */; };
|
||||
E4AE2AD42A228CB900680283 /* Inter-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2AD22A228CA700680283 /* Inter-Medium.otf */; };
|
||||
E4AE2AD52A228CC000680283 /* Inter-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */; };
|
||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
@@ -345,7 +342,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -371,7 +367,6 @@
|
||||
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
@@ -389,7 +384,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -465,7 +459,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -574,7 +567,6 @@
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
|
||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
|
||||
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -618,8 +610,6 @@
|
||||
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
||||
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; };
|
||||
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; };
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
|
||||
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
|
||||
@@ -647,7 +637,6 @@
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
|
||||
4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
|
||||
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
|
||||
@@ -662,7 +651,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -693,7 +681,7 @@
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusView.swift; sourceTree = "<group>"; };
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
|
||||
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
|
||||
@@ -754,8 +742,15 @@
|
||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.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>"; };
|
||||
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
|
||||
E4AE2AC52A227E3600680283 /* Inter-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Bold.otf"; sourceTree = "<group>"; };
|
||||
E4AE2AC62A227E4200680283 /* Inter-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Regular.otf"; sourceTree = "<group>"; };
|
||||
E4AE2AC72A227E5400680283 /* Inter-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Light.otf"; sourceTree = "<group>"; };
|
||||
E4AE2AC82A227E6000680283 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
|
||||
E4AE2ACD2A2286D000680283 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
|
||||
E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Italic.otf"; sourceTree = "<group>"; };
|
||||
E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-SemiBold.otf"; sourceTree = "<group>"; };
|
||||
E4AE2AD22A228CA700680283 /* Inter-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Medium.otf"; sourceTree = "<group>"; };
|
||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -776,7 +771,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -878,7 +872,6 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C9AA1462A444422003F49FD /* Zaps */,
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||
@@ -920,7 +913,6 @@
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
|
||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -957,15 +949,6 @@
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C1A9A2829DDF53B00516EAC /* Video */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
|
||||
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */,
|
||||
);
|
||||
path = Video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -982,7 +965,6 @@
|
||||
children = (
|
||||
4C54AA0929A55429003E4487 /* EventGroup.swift */,
|
||||
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
|
||||
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
@@ -993,7 +975,6 @@
|
||||
4C7D09692A0AEA0400943473 /* CodeScanner */,
|
||||
4C7D095A2A098C5C00943473 /* Wallet */,
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
||||
4C1A9A2829DDF53B00516EAC /* Video */,
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
@@ -1156,8 +1137,7 @@
|
||||
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
|
||||
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
|
||||
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
|
||||
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
|
||||
D2277EE92A089BD5006C3807 /* Router.swift */,
|
||||
E4AE2ACD2A2286D000680283 /* FontManager.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -1171,14 +1151,6 @@
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C9AA1462A444422003F49FD /* Zaps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1188,7 +1160,7 @@
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
|
||||
);
|
||||
@@ -1219,7 +1191,6 @@
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CB8FC222A41ABA500763C51 /* AboutView.swift */,
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
|
||||
@@ -1231,7 +1202,6 @@
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
|
||||
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
|
||||
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
|
||||
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@@ -1310,9 +1280,7 @@
|
||||
4CE6DEE427F7A08100C66700 /* Products */,
|
||||
4CEE2AE62804F57B00AB5EEF /* Frameworks */,
|
||||
);
|
||||
indentWidth = 4;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 4;
|
||||
};
|
||||
4CE6DEE427F7A08100C66700 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
@@ -1327,6 +1295,7 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E40B46262A2272E6005E70FD /* Fonts */,
|
||||
F7F0BA23297892AE009531F3 /* Modifiers */,
|
||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */,
|
||||
4CE4F9DF285287A000C00DD9 /* Components */,
|
||||
@@ -1416,7 +1385,6 @@
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
|
||||
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
@@ -1484,6 +1452,20 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E40B46262A2272E6005E70FD /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E4AE2AC52A227E3600680283 /* Inter-Bold.otf */,
|
||||
E4AE2AC62A227E4200680283 /* Inter-Regular.otf */,
|
||||
E4AE2AC72A227E5400680283 /* Inter-Light.otf */,
|
||||
E4AE2AD12A228CA000680283 /* Inter-SemiBold.otf */,
|
||||
E4AE2AD22A228CA700680283 /* Inter-Medium.otf */,
|
||||
E4AE2ACF2A228C7500680283 /* Inter-Italic.otf */,
|
||||
E4AE2AC82A227E6000680283 /* LICENSE.txt */,
|
||||
);
|
||||
path = Fonts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F7F0BA23297892AE009531F3 /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1519,7 +1501,6 @@
|
||||
packageProductDependencies = (
|
||||
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -1624,7 +1605,6 @@
|
||||
packageReferences = (
|
||||
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1642,10 +1622,17 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E4AE2AD52A228CC000680283 /* Inter-Italic.otf in Resources */,
|
||||
E4AE2AD42A228CB900680283 /* Inter-Medium.otf in Resources */,
|
||||
E4AE2AD32A228CB400680283 /* Inter-SemiBold.otf in Resources */,
|
||||
E4AE2ACC2A227E8300680283 /* Inter-Light.otf in Resources */,
|
||||
E4AE2ACB2A227E7E00680283 /* Inter-Bold.otf in Resources */,
|
||||
E4AE2ACA2A227E7800680283 /* Inter-Regular.otf in Resources */,
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
|
||||
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
||||
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */,
|
||||
E4AE2AC92A227E6000680283 /* LICENSE.txt in Resources */,
|
||||
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
||||
4C198DF129F88C6B004C165C /* License.txt in Resources */,
|
||||
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
|
||||
@@ -1682,7 +1669,6 @@
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */,
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||
@@ -1701,7 +1687,6 @@
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
||||
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||
@@ -1768,6 +1753,7 @@
|
||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */,
|
||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
||||
E4AE2ACE2A2286D000680283 /* FontManager.swift in Sources */,
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
@@ -1786,7 +1772,6 @@
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
|
||||
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */,
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
|
||||
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
|
||||
@@ -1826,11 +1811,9 @@
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
@@ -1865,8 +1848,6 @@
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
@@ -1914,7 +1895,6 @@
|
||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
||||
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */,
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
@@ -1929,12 +1909,11 @@
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
||||
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
||||
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
@@ -1946,7 +1925,6 @@
|
||||
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
|
||||
@@ -2242,7 +2220,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2260,7 +2238,6 @@
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -2291,7 +2268,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2309,7 +2286,6 @@
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -2340,7 +2316,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -2360,7 +2336,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -2465,14 +2441,6 @@
|
||||
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
|
||||
};
|
||||
};
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/wxxsw/GSPlayer";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.2.26;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -2486,11 +2454,6 @@
|
||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||
productName = secp256k1;
|
||||
};
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */;
|
||||
productName = GSPlayer;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "gsplayer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wxxsw/GSPlayer",
|
||||
"state" : {
|
||||
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
|
||||
"version" : "0.2.26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -24,11 +24,9 @@ struct GradientButtonStyle: ButtonStyle {
|
||||
struct GradientButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Button(action: {
|
||||
Button("Dynamic Size", action: {
|
||||
print("dynamic size")
|
||||
}) {
|
||||
Text(verbatim: "Dynamic Size")
|
||||
}
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
|
||||
|
||||
@@ -36,7 +34,7 @@ struct GradientButtonStyle_Previews: PreviewProvider {
|
||||
print("infinite width")
|
||||
}) {
|
||||
HStack {
|
||||
Text(verbatim: "Infinite Width")
|
||||
Text("Infinite Width")
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
@@ -53,9 +53,8 @@ enum ImageShape {
|
||||
}
|
||||
|
||||
// MARK: - Image Carousel
|
||||
@MainActor
|
||||
struct ImageCarousel: View {
|
||||
var urls: [MediaUrl]
|
||||
var urls: [URL]
|
||||
|
||||
let evid: String
|
||||
|
||||
@@ -70,13 +69,11 @@ struct ImageCarousel: View {
|
||||
@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: [MediaUrl]) {
|
||||
init(state: DamusState, evid: String, urls: [URL]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
_image_fill = State(initialValue: media_model.fill)
|
||||
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
@@ -105,108 +102,77 @@ struct ImageCarousel: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
||||
if self.image_fill == nil,
|
||||
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)
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func video_model(_ url: URL) -> VideoPlayerModel {
|
||||
return state.events.get_video_player_model(url: url)
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
Group {
|
||||
switch url {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
||||
.onChange(of: video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
|
||||
}
|
||||
|
||||
var Medias: some View {
|
||||
var Images: some View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
let url = urls[index]
|
||||
GeometryReader { geo in
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
.frame(height: height)
|
||||
.onChange(of: selectedIndex) { value in
|
||||
selectedIndex = value
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
.onChange(of: selectedIndex) { value in
|
||||
selectedIndex = value
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Medias
|
||||
.onTapGesture { }
|
||||
Images
|
||||
|
||||
// This is our custom carousel image indicator
|
||||
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
|
||||
@@ -215,8 +181,8 @@ struct ImageCarousel: View {
|
||||
}
|
||||
|
||||
// MARK: - Custom Carousel
|
||||
struct CarouselDotsView<T>: View {
|
||||
let urls: [T]
|
||||
struct CarouselDotsView: View {
|
||||
let urls: [URL]
|
||||
@Binding var selectedIndex: Int
|
||||
|
||||
var body: some View {
|
||||
@@ -288,8 +254,7 @@ public struct ImageFill {
|
||||
// MARK: - Preview Provider
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ struct InvoiceView: View {
|
||||
var PayButton: some View {
|
||||
Button {
|
||||
if settings.show_wallet_selector {
|
||||
present_sheet(.select_wallet(invoice: invoice.string))
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
||||
}
|
||||
@@ -79,6 +79,9 @@ struct InvoiceView: View {
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +116,3 @@ struct InvoiceView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet, sheet)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ struct Reposted: View {
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ struct TruncatedText: View {
|
||||
let maxChars: Int = 280
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
@@ -28,6 +28,16 @@ struct TruncatedText: View {
|
||||
.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 {
|
||||
|
||||
@@ -11,27 +11,30 @@ struct UserViewRow: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.background(.clear)
|
||||
.background(
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let spacer: Bool
|
||||
|
||||
@State var about_text: Text? = nil
|
||||
|
||||
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
|
||||
self.damus_state = damus_state
|
||||
self.pubkey = pubkey
|
||||
self.spacer = spacer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
@@ -39,16 +42,16 @@ struct UserView: View {
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
|
||||
if let about_text {
|
||||
about_text
|
||||
if let about = profile?.about {
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
Text(about_string)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
if spacer {
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ struct WebsiteLink: View {
|
||||
Text(link_text)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.accentColor)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,31 +10,36 @@ import SwiftUI
|
||||
enum ZappingEventType {
|
||||
case failed(ZappingError)
|
||||
case got_zap_invoice(String)
|
||||
case sent_from_nwc
|
||||
}
|
||||
|
||||
enum ZappingError {
|
||||
case fetching_invoice
|
||||
case bad_lnurl
|
||||
case canceled
|
||||
case send_failed
|
||||
}
|
||||
|
||||
struct ZappingEvent {
|
||||
let is_custom: Bool
|
||||
let type: ZappingEventType
|
||||
let target: ZapTarget
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
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 {
|
||||
let damus_state: DamusState
|
||||
let target: ZapTarget
|
||||
let event: NostrEvent
|
||||
let lnurl: String
|
||||
|
||||
@ObservedObject var zaps: ZapsDataModel
|
||||
@StateObject var button: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
var our_zap: Zapping? {
|
||||
zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey })
|
||||
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
|
||||
}
|
||||
|
||||
var zap_img: String {
|
||||
@@ -55,11 +60,18 @@ struct ZapButton: View {
|
||||
|
||||
// always orange !
|
||||
return Color.orange
|
||||
/*
|
||||
if our_zap.is_paid {
|
||||
return Color.orange
|
||||
} else {
|
||||
return Color.yellow
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func tap() {
|
||||
guard let our_zap else {
|
||||
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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,17 +118,15 @@ struct ZapButton: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
if !damus_state.settings.nozaps || zaps.zap_total > 0 {
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(zap_img)
|
||||
.resizable()
|
||||
.foregroundColor(zap_color)
|
||||
.font(.footnote.weight(.medium))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width:20, height: 20)
|
||||
})
|
||||
}
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(zap_img)
|
||||
.resizable()
|
||||
.foregroundColor(zap_color)
|
||||
.font(.footnote.weight(.medium))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width:20, height: 20)
|
||||
})
|
||||
|
||||
if zaps.zap_total > 0 {
|
||||
Text(verbatim: format_msats_abbrev(zaps.zap_total))
|
||||
@@ -126,15 +136,41 @@ struct ZapButton: View {
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||
guard !damus_state.settings.nozaps else { return }
|
||||
|
||||
present_sheet(.zap(target: target, lnurl: lnurl))
|
||||
button.showing_zap_customizer = true
|
||||
})
|
||||
.highPriorityGesture(TapGesture().onEnded {
|
||||
guard !damus_state.settings.nozaps else { return }
|
||||
|
||||
tap()
|
||||
})
|
||||
.sheet(isPresented: $button.showing_zap_customizer) {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard zap_ev.event.id == self.event.id else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
self.button.invoice = inv
|
||||
self.button.showing_select_wallet = true
|
||||
} else {
|
||||
let wallet = damus_state.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +180,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 zaps = ZapsDataModel([.pending(pending_zap)])
|
||||
|
||||
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +196,14 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
|
||||
return .external(ExtPendingZapState(state: .fetching_invoice))
|
||||
}
|
||||
|
||||
func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
guard let keypair = damus_state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
let content = comment ?? ""
|
||||
|
||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||
@@ -194,7 +231,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -208,7 +245,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -221,35 +258,24 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
// don't both continuing, user has canceled
|
||||
if case .cancel_fetching_invoice = nwc_state.state {
|
||||
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
|
||||
}
|
||||
|
||||
var flusher: OnFlush? = nil
|
||||
|
||||
// donations are only enabled on one-tap zaps and off appstore
|
||||
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
|
||||
// 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
|
||||
Task { @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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||
|
||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -258,13 +284,9 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
|
||||
// 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):
|
||||
pending_ext.state = .done
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
}
|
||||
|
||||
+99
-157
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
@@ -14,38 +13,17 @@ struct TimestampedProfile {
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
struct ZapSheet {
|
||||
let target: ZapTarget
|
||||
let lnurl: String
|
||||
}
|
||||
|
||||
struct SelectWallet {
|
||||
let invoice: String
|
||||
}
|
||||
|
||||
enum Sheets: Identifiable {
|
||||
case post(PostAction)
|
||||
case report(ReportTarget)
|
||||
case event(NostrEvent)
|
||||
case zap(ZapSheet)
|
||||
case select_wallet(SelectWallet)
|
||||
case filter
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||
}
|
||||
|
||||
static func select_wallet(invoice: String) -> Sheets {
|
||||
return .select_wallet(SelectWallet(invoice: invoice))
|
||||
}
|
||||
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post(let action): return "post-" + (action.ev?.id ?? "")
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .zap(let sheet): return "zap-" + sheet.target.id
|
||||
case .select_wallet: return "select-wallet"
|
||||
case .filter: return "filter"
|
||||
}
|
||||
}
|
||||
@@ -82,14 +60,21 @@ struct ContentView: View {
|
||||
@State var damus_state: DamusState? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var active_profile: String? = nil
|
||||
@State var active_search: NostrFilter? = nil
|
||||
@State var active_event: NostrEvent? = nil
|
||||
@State var profile_open: Bool = false
|
||||
@State var thread_open: Bool = false
|
||||
@State var search_open: Bool = false
|
||||
@State var wallet_open: Bool = false
|
||||
@State var active_nwc: WalletConnectURL? = nil
|
||||
@State var muting: String? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
|
||||
let sub_id = UUID().description
|
||||
|
||||
@@ -121,7 +106,7 @@ struct ContentView: View {
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post(.posting(.none))
|
||||
self.active_sheet = .post(.posting)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,8 +114,8 @@ struct ContentView: View {
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
|
||||
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
|
||||
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
||||
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
@@ -142,13 +127,16 @@ struct ContentView: View {
|
||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||
ZStack {
|
||||
if let damus = self.damus_state {
|
||||
TimelineView(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
|
||||
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
navigationCoordinator.popToRoot()
|
||||
profile_open = false
|
||||
thread_open = false
|
||||
search_open = false
|
||||
wallet_open = false
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
@@ -159,6 +147,21 @@ struct ContentView: View {
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
VStack {
|
||||
NavigationLink(destination: WalletView(damus_state: damus, model: damus_state!.wallet), isActive: $wallet_open) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||
EmptyView()
|
||||
}
|
||||
if let active_event {
|
||||
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
|
||||
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
||||
EmptyView()
|
||||
}
|
||||
switch selected_timeline {
|
||||
case .search:
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -200,6 +203,28 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeSearchView: some View {
|
||||
Group {
|
||||
if let search = self.active_search {
|
||||
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeProfileView: some View {
|
||||
Group {
|
||||
if let pk = self.active_profile {
|
||||
let profile_model = ProfileModel(pubkey: pk, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pk)
|
||||
ProfileView(damus_state: damus_state!, profile: profile_model, followers: followers)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let damus_state {
|
||||
@@ -215,30 +240,32 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
let thread = ThreadModel(event: ev, damus_state: damus_state!)
|
||||
navigationCoordinator.push(route: Route.Thread(thread: thread))
|
||||
popToRoot()
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
|
||||
func open_wallet(nwc: WalletConnectURL) {
|
||||
self.damus_state!.wallet.new(nwc)
|
||||
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet))
|
||||
self.wallet_open = true
|
||||
}
|
||||
|
||||
func open_profile(id: String) {
|
||||
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: id)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
popToRoot()
|
||||
self.active_profile = id
|
||||
self.profile_open = true
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
let search = SearchModel(state: damus_state!, search: filt)
|
||||
navigationCoordinator.push(route: Route.Search(search: search))
|
||||
popToRoot()
|
||||
self.active_search = filt
|
||||
self.search_open = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
NavigationStack(path: $navigationCoordinator.path) {
|
||||
NavigationView {
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
@@ -261,14 +288,13 @@ struct ContentView: View {
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
present_sheet(.filter)
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,16 +304,10 @@ struct ContentView: View {
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
navigationCoordinator.popToRoot()
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
}
|
||||
@@ -295,7 +315,6 @@ struct ContentView: View {
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
setup_notifications()
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
@@ -306,10 +325,6 @@ struct ContentView: View {
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .zap(let zapsheet):
|
||||
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
||||
case .select_wallet(let select):
|
||||
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
||||
case .filter:
|
||||
let timeline = selected_timeline
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -417,31 +432,6 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.present_sheet)) { notif in
|
||||
let sheet = notif.object as! Sheets
|
||||
self.active_sheet = sheet
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if damus_state!.settings.show_wallet_selector {
|
||||
present_sheet(.select_wallet(invoice: inv))
|
||||
} else {
|
||||
let wallet = damus_state!.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
}
|
||||
case .sent_from_nwc:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||
switch phase {
|
||||
case .background:
|
||||
@@ -464,11 +454,6 @@ struct ContentView: View {
|
||||
let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
if local.type == .profile_zap {
|
||||
open_profile(id: local.event_id)
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
@@ -477,16 +462,13 @@ struct ContentView: View {
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
damus_state.dms.open_dm_by_pk(target.pubkey)
|
||||
|
||||
case .like: fallthrough
|
||||
case .zap: fallthrough
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
// Handled separately above.
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
|
||||
@@ -517,7 +499,7 @@ struct ContentView: View {
|
||||
}, message: {
|
||||
if let pubkey = self.muting {
|
||||
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||
} else {
|
||||
Text("User has been muted", comment: "Alert message that informs a user was d.")
|
||||
@@ -577,7 +559,7 @@ struct ContentView: View {
|
||||
}, message: {
|
||||
if let pubkey = muting {
|
||||
let profile = damus_state?.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||
} else {
|
||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||
@@ -645,8 +627,7 @@ struct ContentView: View {
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator
|
||||
wallet: WalletModel(settings: settings)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -755,57 +736,24 @@ func setup_notifications() {
|
||||
}
|
||||
}
|
||||
|
||||
struct FindEvent {
|
||||
let type: FindEventType
|
||||
let find_from: [String]?
|
||||
|
||||
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
||||
}
|
||||
|
||||
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .event(evid), find_from: find_from)
|
||||
}
|
||||
}
|
||||
|
||||
enum FindEventType {
|
||||
case profile(String)
|
||||
case event(String)
|
||||
}
|
||||
|
||||
enum FoundEvent {
|
||||
case profile(Profile, NostrEvent)
|
||||
case invalid_profile(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
||||
|
||||
var filter: NostrFilter? = nil
|
||||
let find_from = query_.find_from
|
||||
let query = query_.type
|
||||
|
||||
switch query {
|
||||
case .profile(let pubkey):
|
||||
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
|
||||
callback(.profile(profile.profile, profile.event))
|
||||
return
|
||||
}
|
||||
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
||||
|
||||
case .event(let evid):
|
||||
if let ev = state.events.lookup(evid) {
|
||||
callback(.event(ev))
|
||||
return
|
||||
}
|
||||
|
||||
filter = NostrFilter(ids: [evid], limit: 1)
|
||||
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
|
||||
if let ev = state.events.lookup(evid) {
|
||||
callback(ev)
|
||||
return
|
||||
}
|
||||
|
||||
let subid = UUID().description
|
||||
var attempts: Int = 0
|
||||
|
||||
var has_event = false
|
||||
guard let filter else { return }
|
||||
|
||||
var filter = search_type == .event ? NostrFilter(ids: [evid]) : NostrFilter(authors: [evid])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [.metadata]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
var attempts = 0
|
||||
|
||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
@@ -821,22 +769,15 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
||||
break
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
|
||||
state.pool.unsubscribe(sub_id: subid)
|
||||
|
||||
switch query {
|
||||
case .profile:
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
|
||||
guard let profile else {
|
||||
callback(.invalid_profile(ev))
|
||||
return
|
||||
}
|
||||
callback(.profile(profile, ev))
|
||||
return
|
||||
}
|
||||
if search_type == .profile && ev.known_kind == .metadata {
|
||||
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) {
|
||||
callback(ev)
|
||||
}
|
||||
case .event:
|
||||
callback(.event(ev))
|
||||
} else {
|
||||
callback(ev)
|
||||
}
|
||||
case .eose:
|
||||
if !has_event {
|
||||
@@ -859,11 +800,11 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
||||
}
|
||||
switch timeline {
|
||||
case .home:
|
||||
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where notes and replies appear from those who the user is following.")
|
||||
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
|
||||
case .notifications:
|
||||
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
case .search:
|
||||
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where notes from all connected relay servers appear.")
|
||||
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
case .dms:
|
||||
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
}
|
||||
@@ -961,9 +902,10 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
||||
if ref.key == "p" {
|
||||
result(.profile(ref.ref_id))
|
||||
} else if ref.key == "e" {
|
||||
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
|
||||
guard let res, case .event(let ev) = res else { return }
|
||||
result(.event(ev))
|
||||
find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
result(.event(ev))
|
||||
}
|
||||
}
|
||||
}
|
||||
case .filter(let filt):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2016-2020 The Inter Project Authors.
|
||||
"Inter" is trademark of Rasmus Andersson.
|
||||
https://github.com/rsms/inter
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -70,5 +70,16 @@
|
||||
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string></string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Inter-Regular.otf</string>
|
||||
<string>Inter-Bold.otf</string>
|
||||
<string>Inter-Light.otf</string>
|
||||
<string>Inter-SemiBold.otf</string>
|
||||
<string>Inter-Medium.otf</string>
|
||||
<string>Inter-Italic.otf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject {
|
||||
@Published var our_zap: Zapping?
|
||||
@Published var likes: Int
|
||||
@Published var boosts: Int
|
||||
@Published private(set) var zaps: Int
|
||||
@Published var zaps: Int
|
||||
@Published var zap_total: Int64
|
||||
@Published var replies: Int
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import Foundation
|
||||
class Contacts {
|
||||
private var friends: Set<String> = Set()
|
||||
private var friend_of_friends: Set<String> = Set()
|
||||
/// Tracks which friends are friends of a given pubkey.
|
||||
private var pubkey_to_our_friends = [String : Set<String>]()
|
||||
private var muted: Set<String> = Set()
|
||||
|
||||
let our_pubkey: String
|
||||
@@ -60,10 +58,6 @@ class Contacts {
|
||||
|
||||
func remove_friend(_ pubkey: String) {
|
||||
friends.remove(pubkey)
|
||||
|
||||
pubkey_to_our_friends.forEach {
|
||||
pubkey_to_our_friends[$0.key]?.remove(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
func get_friend_list() -> [String] {
|
||||
@@ -79,15 +73,6 @@ class Contacts {
|
||||
for tag in contact.tags {
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
friend_of_friends.insert(tag[1])
|
||||
|
||||
// Exclude themself and us.
|
||||
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
|
||||
if pubkey_to_our_friends[tag[1]] == nil {
|
||||
pubkey_to_our_friends[tag[1]] = Set<String>()
|
||||
}
|
||||
|
||||
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,11 +96,6 @@ class Contacts {
|
||||
func follow_state(_ pubkey: String) -> FollowState {
|
||||
return is_friend(pubkey) ? .follows : .unfollows
|
||||
}
|
||||
|
||||
/// Gets the list of pubkeys of our friends who follow the given pubkey.
|
||||
func get_friended_followers(_ pubkey: String) -> [String] {
|
||||
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
||||
}
|
||||
}
|
||||
|
||||
func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {
|
||||
|
||||
@@ -30,23 +30,13 @@ struct DamusState {
|
||||
let replies: ReplyCounter
|
||||
let muted_threads: MutedThreadsManager
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
// store generic zap mapping
|
||||
self.zaps.add_zap(zap: zap)
|
||||
let stored = self.events.store_zap(zap: zap)
|
||||
|
||||
// thread zaps
|
||||
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
|
||||
// [nozaps]: thread zaps are only available outside of the app store
|
||||
replies.count_replies(ev)
|
||||
events.add_replies(ev: ev)
|
||||
}
|
||||
|
||||
// associate with events as well
|
||||
return stored
|
||||
return self.events.store_zap(zap: zap)
|
||||
}
|
||||
|
||||
var pubkey: String {
|
||||
@@ -58,5 +48,5 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator()) }
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore())) }
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@ class DirectMessagesModel: ObservableObject {
|
||||
self.active_model = model
|
||||
}
|
||||
|
||||
func open_dm_by_pk(_ pubkey: String) {
|
||||
self.set_active_dm(pubkey)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func open_dm_by_model(_ model: DirectMessageModel) {
|
||||
self.set_active_dm_model(model)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func set_active_dm(_ pubkey: String) {
|
||||
for model in self.dms where model.pubkey == pubkey {
|
||||
self.set_active_dm_model(model)
|
||||
|
||||
+78
-147
@@ -23,7 +23,7 @@ struct NewEventsBits: OptionSet {
|
||||
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||
}
|
||||
|
||||
class HomeModel {
|
||||
class HomeModel: ObservableObject {
|
||||
// Don't trigger a user notification for events older than a certain age
|
||||
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
|
||||
|
||||
@@ -49,10 +49,9 @@ class HomeModel {
|
||||
|
||||
var signal = SignalModel()
|
||||
|
||||
var notifications = NotificationsModel()
|
||||
var notification_status = NotificationStatusModel()
|
||||
var events: EventHolder = EventHolder()
|
||||
var zap_button: ZapButtonModel = ZapButtonModel()
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var events: EventHolder = EventHolder()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
@@ -165,38 +164,65 @@ class HomeModel {
|
||||
}
|
||||
}
|
||||
|
||||
func handle_zap_event(_ ev: NostrEvent) {
|
||||
process_zap_event(damus_state: damus_state, ev: ev) { zapres in
|
||||
guard case .done(let zap) = zapres else { return }
|
||||
|
||||
guard zap.target.pubkey == self.damus_state.keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||
|
||||
if !self.notifications.insert_zap(.zap(zap)) {
|
||||
return
|
||||
}
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert_zap(.zap(zap)) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.damus_state.settings.zap_vibration {
|
||||
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||
if damus_state.settings.zap_vibration {
|
||||
// Generate zap vibration
|
||||
zap_vibrate(zap_amount: zap.invoice.amount)
|
||||
}
|
||||
|
||||
if self.damus_state.settings.zap_notification {
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
switch zap.target {
|
||||
case .profile(let profile_id):
|
||||
create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id)
|
||||
case .note(let note_target):
|
||||
create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id)
|
||||
}
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handle_zap_event(_ ev: NostrEvent) {
|
||||
// These are zap notifications
|
||||
guard let ptag = event_tag(ev, name: "p") else {
|
||||
return
|
||||
}
|
||||
|
||||
let our_keypair = damus_state.keypair
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.notification_status.new_events = new_bits
|
||||
DispatchQueue.main.async {
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -371,12 +397,12 @@ class HomeModel {
|
||||
|
||||
/// Send the initial filters, just our contact list mostly
|
||||
func send_initial_filters(relay_id: String) {
|
||||
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
|
||||
let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
|
||||
pool.send(.subscribe(subscription), to: [relay_id])
|
||||
var filter = NostrFilter(kinds: [.contacts],
|
||||
limit: 1,
|
||||
authors: [damus_state.pubkey])
|
||||
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
|
||||
}
|
||||
|
||||
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
|
||||
func send_home_filters(relay_id: String?) {
|
||||
// TODO: since times should be based on events from a specific relay
|
||||
// perhaps we could mark this in the relay pool somehow
|
||||
@@ -386,7 +412,7 @@ class HomeModel {
|
||||
|
||||
var contacts_filter = NostrFilter(kinds: [.metadata])
|
||||
contacts_filter.authors = friends
|
||||
|
||||
|
||||
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
@@ -441,7 +467,7 @@ class HomeModel {
|
||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
||||
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
||||
|
||||
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||
|
||||
if let relay_id {
|
||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
|
||||
@@ -524,8 +550,8 @@ class HomeModel {
|
||||
|
||||
@discardableResult
|
||||
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
|
||||
if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||
self.notification_status.new_events = new_bits
|
||||
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||
new_events = new_bits
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -557,7 +583,7 @@ class HomeModel {
|
||||
}
|
||||
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
notification_status.new_events = notifs
|
||||
self.new_events = notifs
|
||||
|
||||
if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
|
||||
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
@@ -575,7 +601,7 @@ class HomeModel {
|
||||
|
||||
if !should_debounce_dms {
|
||||
self.incoming_dms.append(ev)
|
||||
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
@@ -585,7 +611,7 @@ class HomeModel {
|
||||
incoming_dms.append(ev)
|
||||
|
||||
dm_debouncer.debounce { [self] in
|
||||
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
@@ -752,7 +778,7 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
|
||||
|
||||
switch validated {
|
||||
case .unknown:
|
||||
Task.detached(priority: .medium) {
|
||||
Task {
|
||||
let result = validate_event(ev: ev)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -773,11 +799,11 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: (() -> Void)? = nil) {
|
||||
guard_valid_event(events: events, ev: ev) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
completion?(nil)
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -785,7 +811,7 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
completion?(profile)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -858,7 +884,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
|
||||
if changed {
|
||||
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
||||
state.pool.connect()
|
||||
notify(.relays_changed, ())
|
||||
}
|
||||
}
|
||||
@@ -1076,12 +1101,13 @@ func zap_notification_title(_ zap: Zap) -> String {
|
||||
}
|
||||
|
||||
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
||||
let src = zap.request.ev
|
||||
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
|
||||
let src = zap.private_request ?? zap.request.ev
|
||||
let anon = event_is_anonymous(ev: src)
|
||||
let pk = anon ? "anon" : src.pubkey
|
||||
let profile = profiles.lookup(id: pk)
|
||||
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
||||
let formattedSats = format_msats_abbrev(zap.invoice.amount)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
|
||||
|
||||
if src.content.isEmpty {
|
||||
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||||
@@ -1092,28 +1118,7 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
@@ -1165,15 +1170,13 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify )
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
let content = NSAttributedString(render_note_content(ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content)
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
} else if type == .like && damus_state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.last?.ref_id,
|
||||
let liked_event = damus_state.events.lookup(evid)
|
||||
{
|
||||
let content = NSAttributedString(render_note_content(ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content)
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
|
||||
@@ -1199,7 +1202,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
case .dm:
|
||||
title = displayName
|
||||
identifier = "myDMNotification"
|
||||
case .zap, .profile_zap:
|
||||
case .zap:
|
||||
// not handled here
|
||||
break
|
||||
}
|
||||
@@ -1221,75 +1224,3 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ProcessZapResult {
|
||||
case already_processed(Zap)
|
||||
case done(Zap)
|
||||
case failed
|
||||
}
|
||||
|
||||
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
|
||||
// These are zap notifications
|
||||
guard let ptag = event_tag(ev, name: "p") else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
// just return the zap if we already have it
|
||||
if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap {
|
||||
completion(.already_processed(z))
|
||||
return
|
||||
}
|
||||
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
completion(.done(zap))
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.profiles.zappers[ptag] = zapper
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
completion(.done(zap))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? {
|
||||
let our_keypair = damus_state.keypair
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
|
||||
return zap
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
enum MentionType {
|
||||
case pubkey
|
||||
case event
|
||||
|
||||
|
||||
var ref: String {
|
||||
switch self {
|
||||
case .pubkey:
|
||||
@@ -495,17 +495,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions:
|
||||
continue
|
||||
}
|
||||
|
||||
if find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) != nil {
|
||||
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
|
||||
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
|
||||
let mention = Mention(index: nil, type: mention_type, ref: ref)
|
||||
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
|
||||
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
} else {
|
||||
let ind = new_tags.count
|
||||
new_tags.append(refid_to_tag(ref))
|
||||
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
|
||||
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
|
||||
let mention = Mention(index: nil, type: mention_type, ref: ref)
|
||||
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// NotificationStatusModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class NotificationStatusModel: ObservableObject {
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
}
|
||||
@@ -21,12 +21,12 @@ class ZapGroup {
|
||||
}
|
||||
|
||||
func zap_requests() -> [NostrEvent] {
|
||||
zaps.map { z in z.request.ev }
|
||||
zaps.map { z in z.request }
|
||||
}
|
||||
|
||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||
for zap in zaps {
|
||||
if !isIncluded(zap.request.ev) {
|
||||
if !isIncluded(zap.request) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class ZapGroup {
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
|
||||
let new_zaps = zaps.filter { isIncluded($0.request.ev) }
|
||||
let new_zaps = zaps.filter { isIncluded($0.request) }
|
||||
guard new_zaps.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class ZapGroup {
|
||||
|
||||
msat_total += zap.amount
|
||||
|
||||
if !zappers.contains(zap.request.ev.pubkey) {
|
||||
zappers.insert(zap.request.ev.pubkey)
|
||||
if !zappers.contains(zap.request.pubkey) {
|
||||
zappers.insert(zap.request.pubkey)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
for zap in incoming_zaps {
|
||||
pks.insert(zap.request.ev.pubkey)
|
||||
pks.insert(zap.request.pubkey)
|
||||
}
|
||||
|
||||
return Array(pks)
|
||||
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
changed = changed || incoming_events.count != count
|
||||
|
||||
count = profile_zaps.zaps.count
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
|
||||
changed = changed || profile_zaps.zaps.count != count
|
||||
|
||||
for el in reactions {
|
||||
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
for el in zaps {
|
||||
count = el.value.zaps.count
|
||||
el.value.zaps = el.value.zaps.filter {
|
||||
isIncluded($0.request.ev)
|
||||
isIncluded($0.request)
|
||||
}
|
||||
changed = changed || el.value.zaps.count != count
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ struct NostrPost {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: parse nostr:{e,p}:pubkey uris as well
|
||||
func parse_post_mention_type(_ p: Parser) -> MentionType? {
|
||||
if parse_char(p, "@") {
|
||||
return .pubkey
|
||||
|
||||
@@ -76,7 +76,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
text_filter.authors = [pubkey]
|
||||
text_filter.limit = 50
|
||||
text_filter.limit = 500
|
||||
|
||||
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
|
||||
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
|
||||
@@ -7,27 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ReportType: String, CustomStringConvertible, CaseIterable {
|
||||
case spam
|
||||
case nudity
|
||||
case profanity
|
||||
enum ReportType: String {
|
||||
case explicit
|
||||
case illegal
|
||||
case spam
|
||||
case impersonation
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .spam:
|
||||
return NSLocalizedString("Spam", comment: "Description of report type for spam.")
|
||||
case .nudity:
|
||||
return NSLocalizedString("Nudity", comment: "Description of report type for nudity.")
|
||||
case .profanity:
|
||||
return NSLocalizedString("Profanity", comment: "Description of report type for profanity.")
|
||||
case .illegal:
|
||||
return NSLocalizedString("Illegal Content", comment: "Description of report type for illegal content.")
|
||||
case .impersonation:
|
||||
return NSLocalizedString("Impersonation", comment: "Description of report type for impersonation.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReportNoteTarget {
|
||||
@@ -47,12 +31,16 @@ struct Report {
|
||||
}
|
||||
|
||||
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
|
||||
var tags: [[String]]
|
||||
switch target {
|
||||
case .user(let pubkey):
|
||||
return [["p", pubkey, type.rawValue]]
|
||||
tags = [["p", pubkey]]
|
||||
case .note(let notet):
|
||||
return [["e", notet.note_id, type.rawValue], ["p", notet.pubkey]]
|
||||
tags = [["e", notet.note_id], ["p", notet.pubkey]]
|
||||
}
|
||||
|
||||
tags.append(["report", type.rawValue])
|
||||
return tags
|
||||
}
|
||||
|
||||
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
|
||||
|
||||
@@ -10,21 +10,15 @@ import Foundation
|
||||
/// manages the lifetime of a thread
|
||||
class ThreadModel: ObservableObject {
|
||||
@Published var event: NostrEvent
|
||||
let original_event: NostrEvent
|
||||
var event_map: Set<NostrEvent>
|
||||
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
self.original_event = event
|
||||
add_event(event)
|
||||
}
|
||||
|
||||
var is_original: Bool {
|
||||
return original_event.id == event.id
|
||||
}
|
||||
|
||||
let damus_state: DamusState
|
||||
|
||||
let profiles_subid = UUID().description
|
||||
@@ -107,10 +101,6 @@ class ThreadModel: ObservableObject {
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
} else if ev.known_kind == .zap {
|
||||
process_zap_event(damus_state: damus_state, ev: ev) { zap in
|
||||
|
||||
}
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev)
|
||||
}
|
||||
@@ -126,10 +116,3 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func get_top_zap(events: EventCache, evid: String) -> Zapping? {
|
||||
return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in
|
||||
!zap.request.marked_hidden
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class UserSettingsStore: ObservableObject {
|
||||
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
|
||||
var default_media_uploader: MediaUploader
|
||||
|
||||
@Setting(key: "show_wallet_selector", default_value: false)
|
||||
@Setting(key: "show_wallet_selector", default_value: true)
|
||||
var show_wallet_selector: Bool
|
||||
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
@@ -126,10 +126,6 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "truncate_timeline_text", default_value: false)
|
||||
var truncate_timeline_text: Bool
|
||||
|
||||
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
|
||||
@Setting(key: "nozaps", default_value: true)
|
||||
var nozaps: Bool
|
||||
|
||||
@Setting(key: "truncate_mention_text", default_value: true)
|
||||
var truncate_mention_text: Bool
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ZapButtonModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapButtonModel: ObservableObject {
|
||||
var invoice: String? = nil
|
||||
@Published var zapping: String = ""
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// CustomizeZapModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class CustomizeZapModel: ObservableObject {
|
||||
@Published var comment: String = ""
|
||||
@Published var custom_amount: String = ""
|
||||
@Published var custom_amount_sats: Int? = nil
|
||||
@Published var zap_type: ZapType = .pub
|
||||
@Published var invoice: String = ""
|
||||
@Published var error: String? = nil
|
||||
@Published var zapping: Bool = false
|
||||
@Published var show_zap_types: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func set_defaults(settings: UserSettingsStore) {
|
||||
self.zap_type = settings.default_zap_type
|
||||
self.custom_amount = String(settings.default_zap_amount)
|
||||
self.custom_amount_sats = settings.default_zap_amount
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,18 @@ class ZapsModel: ObservableObject {
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
case .event(_, let ev):
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
state.events.store_zap(zap: zap)
|
||||
return
|
||||
}
|
||||
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ class Profile: Codable {
|
||||
}
|
||||
|
||||
var damus_donation: Int? {
|
||||
get { return int("damus_donation_v2"); }
|
||||
set(s) { set_int("damus_donation_v2", s) }
|
||||
get { return int("damus_donation"); }
|
||||
set(s) { set_int("damus_donation", s) }
|
||||
}
|
||||
|
||||
var picture: String? {
|
||||
|
||||
@@ -60,13 +60,24 @@ func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
|
||||
if !parse_str(p, "nostr:") {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ref = parse_post_bech32_mention(p) else {
|
||||
|
||||
guard let typ = parse_nostr_ref_uri_type(p) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
return ref
|
||||
|
||||
if !parse_char(p, ":") {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let pk = parse_hexstr(p, len: 64) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: parse relays from nostr uris
|
||||
return ReferencedId(ref_id: pk, relay_id: nil, key: typ)
|
||||
}
|
||||
|
||||
func decode_universal_link(_ s: String) -> NostrLink? {
|
||||
|
||||
@@ -37,9 +37,9 @@ public struct RelayURL: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
final class RelayConnection: ObservableObject {
|
||||
@Published private(set) var isConnected = false
|
||||
@Published private(set) var isConnecting = false
|
||||
final class RelayConnection {
|
||||
private(set) var isConnected = false
|
||||
private(set) var isConnecting = false
|
||||
|
||||
private(set) var last_connection_attempt: TimeInterval = 0
|
||||
private(set) var last_pong: Date? = nil
|
||||
@@ -129,11 +129,6 @@ final class RelayConnection: ObservableObject {
|
||||
}
|
||||
case .error(let error):
|
||||
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
|
||||
let nserr = error as NSError
|
||||
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
|
||||
// ignore socket not connected?
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isConnected = false
|
||||
self.isConnecting = false
|
||||
|
||||
@@ -18,16 +18,11 @@ struct QueuedRequest {
|
||||
let relay: String
|
||||
}
|
||||
|
||||
struct SeenEvent: Hashable {
|
||||
let relay_id: String
|
||||
let evid: String
|
||||
}
|
||||
|
||||
class RelayPool {
|
||||
var relays: [Relay] = []
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<SeenEvent> = Set()
|
||||
var seen: Set<String> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
|
||||
private let network_monitor = NWPathMonitor()
|
||||
@@ -238,7 +233,7 @@ class RelayPool {
|
||||
func record_seen(relay_id: String, event: NostrConnectionEvent) {
|
||||
if case .nostr_event(let ev) = event {
|
||||
if case .event(_, let nev) = ev {
|
||||
let k = SeenEvent(relay_id: relay_id, evid: nev.id)
|
||||
let k = relay_id + nev.id
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k)
|
||||
if counts[relay_id] == nil {
|
||||
|
||||
@@ -13,19 +13,6 @@ enum WebSocketEvent {
|
||||
case message(URLSessionWebSocketTask.Message)
|
||||
case disconnected(URLSessionWebSocketTask.CloseCode, String?)
|
||||
case error(Error)
|
||||
|
||||
var description: String? {
|
||||
switch self {
|
||||
case .connected:
|
||||
return "Connected"
|
||||
case .message(_):
|
||||
return "Received message"
|
||||
case .disconnected(let close_code, let reason):
|
||||
return "Disconnected: Close code: \(close_code), reason: \(reason ?? "unknown")"
|
||||
case .error(let error):
|
||||
return "Error: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class WebSocket: NSObject, URLSessionWebSocketDelegate {
|
||||
|
||||
@@ -38,7 +38,7 @@ enum DisplayName {
|
||||
|
||||
|
||||
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
|
||||
if pubkey == ANON_PUBKEY {
|
||||
if pubkey == "anon" {
|
||||
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class ZapsDataModel: ObservableObject {
|
||||
}
|
||||
|
||||
func confirm_nwc(reqid: String) {
|
||||
guard let zap = zaps.first(where: { z in z.request.ev.id == reqid }),
|
||||
guard let zap = zaps.first(where: { z in z.request.id == reqid }),
|
||||
case .pending(let pzap) = zap
|
||||
else {
|
||||
return
|
||||
@@ -83,16 +83,16 @@ class ZapsDataModel: ObservableObject {
|
||||
}
|
||||
|
||||
func from(_ pubkey: String) -> [Zapping] {
|
||||
return self.zaps.filter { z in z.request.ev.pubkey == pubkey }
|
||||
return self.zaps.filter { z in z.request.pubkey == pubkey }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func remove(reqid: String) -> Bool {
|
||||
guard zaps.first(where: { z in z.request.ev.id == reqid }) != nil else {
|
||||
guard zaps.first(where: { z in z.request.id == reqid }) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.zaps = zaps.filter { z in z.request.ev.id != reqid }
|
||||
self.zaps = zaps.filter { z in z.request.id != reqid }
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,6 @@ class RelativeTimeModel: ObservableObject {
|
||||
@Published var value: String = ""
|
||||
}
|
||||
|
||||
class MediaMetaModel: ObservableObject {
|
||||
@Published var fill: ImageFill? = nil
|
||||
}
|
||||
|
||||
class EventData {
|
||||
var translations_model: TranslationModel
|
||||
var artifacts_model: NoteArtifactsModel
|
||||
@@ -112,7 +108,6 @@ class EventData {
|
||||
var zaps_model : ZapsDataModel
|
||||
var relative_time: RelativeTimeModel = RelativeTimeModel()
|
||||
var validated: ValidationResult
|
||||
var media_metadata_model: MediaMetaModel
|
||||
|
||||
var translations: TranslateStatus {
|
||||
return translations_model.state
|
||||
@@ -131,7 +126,6 @@ class EventData {
|
||||
self.artifacts_model = .init(state: .not_loaded)
|
||||
self.zaps_model = .init(zaps)
|
||||
self.validated = .unknown
|
||||
self.media_metadata_model = MediaMetaModel()
|
||||
self.preview_model = .init(state: .not_loaded)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +135,6 @@ class EventCache {
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var image_metadata: [String: ImageMetadataState] = [:]
|
||||
private var video_meta: [String: VideoPlayerModel] = [:]
|
||||
private var event_data: [String: EventData] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
@@ -175,9 +168,6 @@ class EventCache {
|
||||
@discardableResult
|
||||
func store_zap(zap: Zapping) -> Bool {
|
||||
let data = get_cache_data(zap.target.id).zaps_model
|
||||
if let ev = zap.event {
|
||||
insert(ev)
|
||||
}
|
||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
||||
}
|
||||
|
||||
@@ -185,7 +175,7 @@ class EventCache {
|
||||
switch zap.target {
|
||||
case .note(let note_target):
|
||||
let zaps = get_cache_data(note_target.note_id).zaps_model
|
||||
zaps.remove(reqid: zap.request.ev.id)
|
||||
zaps.remove(reqid: zap.request.id)
|
||||
case .profile:
|
||||
// these aren't stored anywhere yet
|
||||
break
|
||||
@@ -204,30 +194,6 @@ class EventCache {
|
||||
return image_metadata[url.absoluteString.lowercased()]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func lookup_media_size(url: URL) -> CGSize? {
|
||||
if let img_meta = lookup_img_metadata(url: url) {
|
||||
return img_meta.meta.dim?.size
|
||||
}
|
||||
|
||||
return get_video_player_model(url: url).size
|
||||
}
|
||||
|
||||
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
|
||||
video_meta[url.absoluteString] = meta
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func get_video_player_model(url: URL) -> VideoPlayerModel {
|
||||
if let model = video_meta[url.absoluteString] {
|
||||
return model
|
||||
}
|
||||
|
||||
let model = VideoPlayerModel()
|
||||
video_meta[url.absoluteString] = model
|
||||
return model
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
@@ -291,7 +257,6 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
video_meta = [:]
|
||||
event_data = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
@@ -400,7 +365,7 @@ func preload_image(url: URL) {
|
||||
|
||||
print("Preloading image \(url.absoluteString)")
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
|
||||
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in
|
||||
print("Preloaded image \(url.absoluteString)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
class EventHolder: ObservableObject, ScrollQueue {
|
||||
private var has_event: Set<String>
|
||||
@Published var events: [NostrEvent]
|
||||
var incoming: [NostrEvent]
|
||||
@Published var incoming: [NostrEvent]
|
||||
var should_queue: Bool
|
||||
var on_queue: ((NostrEvent) -> Void)?
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ extension KFOptionSetter {
|
||||
|
||||
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
|
||||
guard let url = fallbackUrl, let key = cacheKey else { return self }
|
||||
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
|
||||
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
|
||||
let source = imageResource.convertToSource()
|
||||
options.alternativeSources = [source]
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// FontManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Ben Weeks on 27/05/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FontManager {
|
||||
struct dynamicSize {
|
||||
public static var largeTitle: CGFloat = UIFont.preferredFont(forTextStyle: .largeTitle).pointSize - 1
|
||||
public static var title1: CGFloat = UIFont.preferredFont(forTextStyle: .title1).pointSize - 0
|
||||
public static var title2: CGFloat = UIFont.preferredFont(forTextStyle: .title2).pointSize - 0
|
||||
public static var title3: CGFloat = UIFont.preferredFont(forTextStyle: .title3).pointSize - 0
|
||||
public static var body: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize - 1
|
||||
public static var callout: CGFloat = UIFont.preferredFont(forTextStyle: .callout).pointSize - 1
|
||||
public static var caption1: CGFloat = UIFont.preferredFont(forTextStyle: .caption1).pointSize - 1
|
||||
public static var caption2: CGFloat = UIFont.preferredFont(forTextStyle: .caption2).pointSize - 1
|
||||
public static var footnote: CGFloat = UIFont.preferredFont(forTextStyle: .footnote).pointSize - 1
|
||||
public static var headline: CGFloat = UIFont.preferredFont(forTextStyle: .headline).pointSize - 1
|
||||
public static var subheadline: CGFloat = UIFont.preferredFont(forTextStyle: .subheadline).pointSize - 1
|
||||
// repeat for all the dynamic sizes
|
||||
}
|
||||
|
||||
struct Inter {
|
||||
static let familyRoot = "Inter"
|
||||
|
||||
static let bold = "\(familyRoot)-Bold"
|
||||
static let regular = "\(familyRoot)-Regular"
|
||||
static let light = "\(familyRoot)-Light"
|
||||
static let medium = "\(familyRoot)-Medium"
|
||||
static let semibold = "\(familyRoot)-SemiBold"
|
||||
static let italic = "\(familyRoot)-Italic"
|
||||
|
||||
static let largeTitle: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.largeTitle)
|
||||
static let title1: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title1)
|
||||
static let title2: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title2)
|
||||
static let title3: Font = Font.custom(FontManager.Inter.semibold, size: FontManager.dynamicSize.title3)
|
||||
static let body: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.body)
|
||||
static let caption1: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.caption1)
|
||||
static let caption2: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.caption2)
|
||||
static let footnote: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.footnote)
|
||||
static let headline: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.headline)
|
||||
static let subheadline: Font = Font.custom(FontManager.Inter.regular, size: FontManager.dynamicSize.subheadline)
|
||||
// repeat for other sizes
|
||||
}
|
||||
}
|
||||
|
||||
extension Font {
|
||||
public static var largeTitle = FontManager.Inter.largeTitle
|
||||
public static var title1 = FontManager.Inter.title1
|
||||
public static var title2 = FontManager.Inter.title2
|
||||
public static var title3 = FontManager.Inter.title3
|
||||
public static var body = FontManager.Inter.body
|
||||
public static var caption1 = FontManager.Inter.caption1
|
||||
public static var caption2 = FontManager.Inter.caption2
|
||||
public static var footnote = FontManager.Inter.footnote
|
||||
public static var headline = FontManager.Inter.headline
|
||||
public static var subheadline = FontManager.Inter.subheadline
|
||||
// repeat for the rest of the dynamic sizes
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func hashtag_str(_ htag: String) -> CompatibleText {
|
||||
}
|
||||
text = Text(attributedString)
|
||||
let img = Image("\(name)-hashtag")
|
||||
text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0)
|
||||
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
|
||||
} else {
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
}
|
||||
|
||||
@@ -59,15 +59,17 @@ struct ImageMetadata: Equatable {
|
||||
}
|
||||
|
||||
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
|
||||
let res = Task.detached(priority: .low) {
|
||||
let res = Task.init {
|
||||
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
|
||||
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
|
||||
let noimg: UIImage? = nil
|
||||
return noimg
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
return await res.value
|
||||
}
|
||||
|
||||
@@ -144,7 +146,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
|
||||
return nil
|
||||
}
|
||||
|
||||
let res = Task.detached(priority: .low) {
|
||||
let res = Task.init {
|
||||
let bhs = get_blurhash_size(img_size: img.size)
|
||||
let smaller = img.resized(to: bhs)
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi
|
||||
var i: Int = 0
|
||||
|
||||
for zap in zaps {
|
||||
if new_zap.request.ev.id == zap.request.ev.id {
|
||||
if new_zap.request.id == zap.request.id {
|
||||
// replace pending
|
||||
if !new_zap.is_pending && zap.is_pending {
|
||||
print("nwc: replacing pending with real zap \(new_zap.request.ev.id)")
|
||||
print("nwc: replacing pending with real zap \(new_zap.request.id)")
|
||||
zaps[i] = new_zap
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import Foundation
|
||||
import secp256k1
|
||||
|
||||
let PUBKEY_HRP = "npub"
|
||||
let ANON_PUBKEY = "anon"
|
||||
|
||||
struct FullKeypair: Equatable {
|
||||
let pubkey: String
|
||||
|
||||
@@ -44,5 +44,4 @@ enum LocalNotificationType: String {
|
||||
case mention
|
||||
case repost
|
||||
case zap
|
||||
case profile_zap
|
||||
}
|
||||
|
||||
@@ -77,9 +77,6 @@ extension Notification.Name {
|
||||
static var update_stats: Notification.Name {
|
||||
return Notification.Name("update_stats")
|
||||
}
|
||||
static var present_sheet: Notification.Name {
|
||||
return Notification.Name("present_sheet")
|
||||
}
|
||||
static var zapping: Notification.Name {
|
||||
return Notification.Name("zapping")
|
||||
}
|
||||
|
||||
@@ -66,12 +66,22 @@ enum PreviewState {
|
||||
|
||||
class PreviewCache {
|
||||
private var previews: [String: Preview]
|
||||
private var image_meta: [String: ImageFill]
|
||||
|
||||
func lookup(_ evid: String) -> Preview? {
|
||||
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() {
|
||||
self.previews = [:]
|
||||
self.image_meta = [:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
//
|
||||
// Router.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Scott Penrose on 5/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum Route: Hashable {
|
||||
case ProfileByKey(pubkey: String)
|
||||
case Profile(profile: ProfileModel, followers: FollowersModel)
|
||||
case Followers(followers: FollowersModel)
|
||||
case Relay(relay: String, showActionButtons: Binding<Bool>)
|
||||
case RelayDetail(relay: String, metadata: RelayMetadata)
|
||||
case Following(following: FollowingModel)
|
||||
case MuteList(users: [String])
|
||||
case RelayConfig
|
||||
case Bookmarks
|
||||
case Config
|
||||
case EditMetadata
|
||||
case DMChat(dms: DirectMessageModel)
|
||||
case UserRelays(relays: [String])
|
||||
case KeySettings(keypair: Keypair)
|
||||
case AppearanceSettings(settings: UserSettingsStore)
|
||||
case NotificationSettings(settings: UserSettingsStore)
|
||||
case ZapSettings(settings: UserSettingsStore)
|
||||
case TranslationSettings(settings: UserSettingsStore)
|
||||
case SearchSettings(settings: UserSettingsStore)
|
||||
case Thread(thread: ThreadModel)
|
||||
case Reposts(reposts: RepostsModel)
|
||||
case Reactions(reactions: ReactionsModel)
|
||||
case Zaps(target: ZapTarget)
|
||||
case Search(search: SearchModel)
|
||||
case EULA
|
||||
case Login
|
||||
case CreateAccount
|
||||
case SaveKeys(account: CreateAccountModel)
|
||||
case Wallet(wallet: WalletModel)
|
||||
case WalletScanner(result: Binding<WalletScanResult>)
|
||||
case FollowersYouKnow(friendedFollowers: [String], followers: FollowersModel)
|
||||
|
||||
@ViewBuilder
|
||||
func view(navigationCordinator: NavigationCoordinator, damusState: DamusState) -> some View {
|
||||
switch self {
|
||||
case .ProfileByKey(let pubkey):
|
||||
ProfileView(damus_state: damusState, pubkey: pubkey)
|
||||
case .Profile(let profile, let followers):
|
||||
ProfileView(damus_state: damusState, profile: profile, followers: followers)
|
||||
case .Followers(let followers):
|
||||
FollowersView(damus_state: damusState, followers: followers)
|
||||
case .Relay(let relay, let showActionButtons):
|
||||
RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons)
|
||||
case .RelayDetail(let relay, let metadata):
|
||||
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
|
||||
case .Following(let following):
|
||||
FollowingView(damus_state: damusState, following: following)
|
||||
case .MuteList(let users):
|
||||
MutelistView(damus_state: damusState, users: users)
|
||||
case .RelayConfig:
|
||||
RelayConfigView(state: damusState)
|
||||
case .Bookmarks:
|
||||
BookmarksView(state: damusState)
|
||||
case .Config:
|
||||
ConfigView(state: damusState)
|
||||
case .EditMetadata:
|
||||
EditMetadataView(damus_state: damusState)
|
||||
case .DMChat(let dms):
|
||||
DMChatView(damus_state: damusState, dms: dms)
|
||||
case .UserRelays(let relays):
|
||||
UserRelaysView(state: damusState, relays: relays)
|
||||
case .KeySettings(let keypair):
|
||||
KeySettingsView(keypair: keypair)
|
||||
case .AppearanceSettings(let settings):
|
||||
AppearanceSettingsView(settings: settings)
|
||||
case .NotificationSettings(let settings):
|
||||
NotificationSettingsView(settings: settings)
|
||||
case .ZapSettings(let settings):
|
||||
ZapSettingsView(settings: settings)
|
||||
case .TranslationSettings(let settings):
|
||||
NotificationSettingsView(settings: settings)
|
||||
case .SearchSettings(let settings):
|
||||
SearchSettingsView(settings: settings)
|
||||
case .Thread(let thread):
|
||||
ThreadView(state: damusState, thread: thread)
|
||||
case .Reposts(let reposts):
|
||||
RepostsView(damus_state: damusState, model: reposts)
|
||||
case .Reactions(let reactions):
|
||||
ReactionsView(damus_state: damusState, model: reactions)
|
||||
case .Zaps(let target):
|
||||
ZapsView(state: damusState, target: target)
|
||||
case .Search(let search):
|
||||
SearchView(appstate: damusState, search: search)
|
||||
case .EULA:
|
||||
EULAView(nav: navigationCordinator)
|
||||
case .Login:
|
||||
LoginView(nav: navigationCordinator)
|
||||
case .CreateAccount:
|
||||
CreateAccountView(nav: navigationCordinator)
|
||||
case .SaveKeys(let account):
|
||||
SaveKeysView(account: account)
|
||||
case .Wallet(let walletModel):
|
||||
WalletView(damus_state: damusState, model: walletModel)
|
||||
case .WalletScanner(let walletScanResult):
|
||||
WalletScannerView(result: walletScanResult)
|
||||
case .FollowersYouKnow(let friendedFollowers, let followers):
|
||||
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: Route, rhs: Route) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.ProfileByKey (let lhs_pubkey), .ProfileByKey(let rhs_pubkey)):
|
||||
return lhs_pubkey == rhs_pubkey
|
||||
case (.Profile (let lhs_profile, _), .Profile(let rhs_profile, _)):
|
||||
return lhs_profile == rhs_profile
|
||||
case (.Followers (_), .Followers (_)):
|
||||
return true
|
||||
case (.Relay (let lhs_relay, _), .Relay (let rhs_relay, _)):
|
||||
return lhs_relay == rhs_relay
|
||||
case (.RelayDetail(let lhs_relay, _), .RelayDetail(let rhs_relay, _)):
|
||||
return lhs_relay == rhs_relay
|
||||
case (.Following(_), .Following(_)):
|
||||
return true
|
||||
case (.MuteList(let lhs_users), .MuteList(let rhs_users)):
|
||||
return lhs_users == rhs_users
|
||||
case (.RelayConfig, .RelayConfig):
|
||||
return true
|
||||
case (.Bookmarks, .Bookmarks):
|
||||
return true
|
||||
case (.Config, .Config):
|
||||
return true
|
||||
case (.EditMetadata, .EditMetadata):
|
||||
return true
|
||||
case (.DMChat(let lhs_dms), .DMChat(let rhs_dms)):
|
||||
return lhs_dms.our_pubkey == rhs_dms.our_pubkey
|
||||
case (.UserRelays(let lhs_relays), .UserRelays(let rhs_relays)):
|
||||
return lhs_relays == rhs_relays
|
||||
case (.KeySettings(let lhs_keypair), .KeySettings(let rhs_keypair)):
|
||||
return lhs_keypair.pubkey == rhs_keypair.pubkey
|
||||
case (.AppearanceSettings(_), .AppearanceSettings(_)):
|
||||
return true
|
||||
case (.NotificationSettings(_), .NotificationSettings(_)):
|
||||
return true
|
||||
case (.ZapSettings(_), .ZapSettings(_)):
|
||||
return true
|
||||
case (.TranslationSettings(_), .TranslationSettings(_)):
|
||||
return true
|
||||
case (.SearchSettings(_), .SearchSettings(_)):
|
||||
return true
|
||||
case (.Thread(let lhs_threadModel), .Thread(thread: let rhs_threadModel)):
|
||||
return lhs_threadModel.event.id == rhs_threadModel.event.id
|
||||
case (.Reposts(let lhs_reposts), .Reposts(let rhs_reposts)):
|
||||
return lhs_reposts.target == rhs_reposts.target
|
||||
case (.Reactions(let lhs_reactions), .Reactions(let rhs_reactions)):
|
||||
return lhs_reactions.target == rhs_reactions.target
|
||||
case (.Zaps(let lhs_target), .Zaps(let rhs_target)):
|
||||
return lhs_target == rhs_target
|
||||
case (.Search(let lhs_search), .Search(let rhs_search)):
|
||||
return lhs_search.sub_id == rhs_search.sub_id && lhs_search.profiles_subid == rhs_search.profiles_subid
|
||||
case (.EULA, .EULA):
|
||||
return true
|
||||
case (.Login, .Login):
|
||||
return true
|
||||
case (.CreateAccount, .CreateAccount):
|
||||
return true
|
||||
case (.SaveKeys(let lhs_account), .SaveKeys(let rhs_account)):
|
||||
return lhs_account.pubkey == rhs_account.pubkey
|
||||
case (.Wallet(_), .Wallet(_)):
|
||||
return true
|
||||
case (.WalletScanner(_), .WalletScanner(_)):
|
||||
return true
|
||||
case (.FollowersYouKnow(_, _), .FollowersYouKnow(_, _)):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .ProfileByKey(let pubkey):
|
||||
hasher.combine("profilebykey")
|
||||
hasher.combine(pubkey)
|
||||
case .Profile(let profile, _):
|
||||
hasher.combine("profile")
|
||||
hasher.combine(profile.pubkey)
|
||||
case .Followers(_):
|
||||
hasher.combine("followers")
|
||||
case .Relay(let relay, _):
|
||||
hasher.combine("relay")
|
||||
hasher.combine(relay)
|
||||
case .RelayDetail(let relay, _):
|
||||
hasher.combine("relayDetail")
|
||||
hasher.combine(relay)
|
||||
case .Following(_):
|
||||
hasher.combine("following")
|
||||
case .MuteList(let users):
|
||||
hasher.combine("muteList")
|
||||
hasher.combine(users)
|
||||
case .RelayConfig:
|
||||
hasher.combine("relayConfig")
|
||||
case .Bookmarks:
|
||||
hasher.combine("bookmarks")
|
||||
case .Config:
|
||||
hasher.combine("config")
|
||||
case .EditMetadata:
|
||||
hasher.combine("editMetadata")
|
||||
case .DMChat(let dms):
|
||||
hasher.combine("dms")
|
||||
hasher.combine(dms.our_pubkey)
|
||||
case .UserRelays(let relays):
|
||||
hasher.combine("userRelays")
|
||||
hasher.combine(relays)
|
||||
case .KeySettings(let keypair):
|
||||
hasher.combine("keySettings")
|
||||
hasher.combine(keypair.pubkey)
|
||||
case .AppearanceSettings(_):
|
||||
hasher.combine("appearanceSettings")
|
||||
case .NotificationSettings(_):
|
||||
hasher.combine("notificationSettings")
|
||||
case .ZapSettings(_):
|
||||
hasher.combine("zapSettings")
|
||||
case .TranslationSettings(_):
|
||||
hasher.combine("translationSettings")
|
||||
case .SearchSettings(_):
|
||||
hasher.combine("searchSettings")
|
||||
case .Thread(let threadModel):
|
||||
hasher.combine("thread")
|
||||
hasher.combine(threadModel.event.id)
|
||||
case .Reposts(let reposts):
|
||||
hasher.combine("reposts")
|
||||
hasher.combine(reposts.target)
|
||||
case .Zaps(let target):
|
||||
hasher.combine("zaps")
|
||||
hasher.combine(target.id)
|
||||
hasher.combine(target.pubkey)
|
||||
case .Reactions(let reactions):
|
||||
hasher.combine("reactions")
|
||||
hasher.combine(reactions.target)
|
||||
case .Search(let search):
|
||||
hasher.combine("search")
|
||||
hasher.combine(search.sub_id)
|
||||
hasher.combine(search.profiles_subid)
|
||||
case .EULA:
|
||||
hasher.combine("eula")
|
||||
case .Login:
|
||||
hasher.combine("login")
|
||||
case .CreateAccount:
|
||||
hasher.combine("createAccount")
|
||||
case .SaveKeys(let account):
|
||||
hasher.combine("saveKeys")
|
||||
hasher.combine(account.pubkey)
|
||||
case .Wallet(_):
|
||||
hasher.combine("wallet")
|
||||
case .WalletScanner(_):
|
||||
hasher.combine("walletScanner")
|
||||
case .FollowersYouKnow(let friendedFollowers, let followers):
|
||||
hasher.combine("followersYouKnow")
|
||||
hasher.combine(friendedFollowers)
|
||||
hasher.combine(followers.sub_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationCoordinator: ObservableObject {
|
||||
@Published var path = [Route]()
|
||||
|
||||
func push(route: Route) {
|
||||
path.append(route)
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
path = []
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// 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) + "..."
|
||||
}
|
||||
}
|
||||
+9
-28
@@ -41,16 +41,7 @@ public enum ZapTarget: Equatable {
|
||||
|
||||
struct ZapRequest {
|
||||
let ev: NostrEvent
|
||||
let marked_hidden: Bool
|
||||
|
||||
var is_in_thread: Bool {
|
||||
return !self.ev.content.isEmpty && !marked_hidden
|
||||
}
|
||||
|
||||
init(ev: NostrEvent) {
|
||||
self.ev = ev
|
||||
self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0] == "hidden" }) != nil
|
||||
}
|
||||
}
|
||||
|
||||
enum ExtPendingZapStateType {
|
||||
@@ -138,7 +129,7 @@ struct ZapRequestId: Equatable {
|
||||
let reqid: String
|
||||
|
||||
init(from_zap: Zapping) {
|
||||
self.reqid = from_zap.request.ev.id
|
||||
self.reqid = from_zap.request.id
|
||||
}
|
||||
|
||||
init(from_makezap: MakeZapRequest) {
|
||||
@@ -207,12 +198,12 @@ enum Zapping {
|
||||
}
|
||||
}
|
||||
|
||||
var request: ZapRequest {
|
||||
var request: NostrEvent {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.request
|
||||
return zap.request_ev
|
||||
case .pending(let pzap):
|
||||
return pzap.request
|
||||
return pzap.request.ev
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,15 +227,6 @@ enum Zapping {
|
||||
}
|
||||
}
|
||||
|
||||
var is_in_thread: Bool {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.request.is_in_thread
|
||||
case .pending(let pzap):
|
||||
return pzap.request.is_in_thread
|
||||
}
|
||||
}
|
||||
|
||||
var is_anon: Bool {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
@@ -260,12 +242,12 @@ struct Zap {
|
||||
public let invoice: ZapInvoice
|
||||
public let zapper: String /// zap authorizer
|
||||
public let target: ZapTarget
|
||||
public let raw_request: ZapRequest
|
||||
public let request: ZapRequest
|
||||
public let is_anon: Bool
|
||||
public let private_request: ZapRequest?
|
||||
public let private_request: NostrEvent?
|
||||
|
||||
var request: ZapRequest {
|
||||
return private_request ?? self.raw_request
|
||||
var request_ev: NostrEvent {
|
||||
return private_request ?? self.request.ev
|
||||
}
|
||||
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
|
||||
@@ -313,9 +295,8 @@ struct Zap {
|
||||
}
|
||||
|
||||
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
|
||||
let preq = private_request.map { pr in ZapRequest(ev: pr) }
|
||||
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, raw_request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: preq)
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-11
@@ -12,8 +12,8 @@ class Zaps {
|
||||
let our_pubkey: String
|
||||
var our_zaps: [String: [Zapping]]
|
||||
|
||||
private(set) var event_counts: [String: Int]
|
||||
private(set) var event_totals: [String: Int64]
|
||||
var event_counts: [String: Int]
|
||||
var event_totals: [String: Int64]
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.zaps = [:]
|
||||
@@ -27,13 +27,13 @@ class Zaps {
|
||||
var res: Zapping? = nil
|
||||
for kv in our_zaps {
|
||||
let ours = kv.value
|
||||
guard let zap = ours.first(where: { z in z.request.ev.id == reqid }) else {
|
||||
guard let zap = ours.first(where: { z in z.request.id == reqid }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
res = zap
|
||||
|
||||
our_zaps[kv.key] = ours.filter { z in z.request.ev.id != reqid }
|
||||
our_zaps[kv.key] = ours.filter { z in z.request.id != reqid }
|
||||
|
||||
if let count = event_counts[zap.target.id] {
|
||||
event_counts[zap.target.id] = count - 1
|
||||
@@ -51,16 +51,13 @@ class Zaps {
|
||||
}
|
||||
|
||||
func add_zap(zap: Zapping) {
|
||||
if zaps[zap.request.ev.id] != nil {
|
||||
if zaps[zap.request.id] != nil {
|
||||
return
|
||||
}
|
||||
self.zaps[zap.request.ev.id] = zap
|
||||
if let zap_id = zap.event?.id {
|
||||
self.zaps[zap_id] = zap
|
||||
}
|
||||
self.zaps[zap.request.id] = zap
|
||||
|
||||
// record our zaps for an event
|
||||
if zap.request.ev.pubkey == our_pubkey {
|
||||
if zap.request.pubkey == our_pubkey {
|
||||
switch zap.target {
|
||||
case .note(let note_target):
|
||||
if our_zaps[note_target.note_id] == nil {
|
||||
@@ -74,7 +71,7 @@ class Zaps {
|
||||
}
|
||||
|
||||
// don't count tips to self. lame.
|
||||
guard zap.request.ev.pubkey != zap.target.pubkey else {
|
||||
guard zap.request.pubkey != zap.target.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ struct EventActionBar: View {
|
||||
self.show_repost_action = true
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
|
||||
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
|
||||
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
@@ -88,14 +88,14 @@ struct EventActionBar: View {
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
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)
|
||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
EventActionButton(img: "upload", col: Color.gray) {
|
||||
show_share_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
|
||||
}
|
||||
.onAppear {
|
||||
self.bar.update(damus: damus_state, evid: self.event.id)
|
||||
|
||||
@@ -25,7 +25,7 @@ struct EventDetailBar: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
if bar.boosts > 0 {
|
||||
NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) {
|
||||
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
|
||||
let noun = Text(verbatim: repostsCountString(bar.boosts)).foregroundColor(.gray)
|
||||
Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
|
||||
}
|
||||
@@ -33,7 +33,7 @@ struct EventDetailBar: View {
|
||||
}
|
||||
|
||||
if bar.likes > 0 && !state.settings.onlyzaps_mode {
|
||||
NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) {
|
||||
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
||||
let noun = Text(verbatim: reactionsCountString(bar.likes)).foregroundColor(.gray)
|
||||
Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||
}
|
||||
@@ -41,7 +41,8 @@ struct EventDetailBar: View {
|
||||
}
|
||||
|
||||
if bar.zaps > 0 {
|
||||
NavigationLink(value: Route.Zaps(target: .note(id: target, author: target_pk))) {
|
||||
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
|
||||
NavigationLink(destination: dst) {
|
||||
let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray)
|
||||
Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ struct BookmarksView: View {
|
||||
} else {
|
||||
ScrollView {
|
||||
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ let carousel_items = [
|
||||
CarouselItem(image: Image("undercover"),
|
||||
text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")),
|
||||
CarouselItem(image: Image("bitcoin-p2p"),
|
||||
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friends and stack sats with Bitcoin⚡️, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string."))
|
||||
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string."))
|
||||
]
|
||||
|
||||
struct CarouselView: View {
|
||||
|
||||
@@ -18,16 +18,16 @@ struct ConfigView: View {
|
||||
@State var delete_account_warning: Bool = false
|
||||
@State var confirm_delete_account: Bool = false
|
||||
@State var delete_text: String = ""
|
||||
|
||||
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
private let DELETE_KEYWORD = "DELETE"
|
||||
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
_settings = ObservedObject(initialValue: state.settings)
|
||||
}
|
||||
|
||||
|
||||
func textColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
@@ -36,30 +36,31 @@ struct ConfigView: View {
|
||||
ZStack(alignment: .leading) {
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(value: Route.KeySettings(keypair: state.keypair)) {
|
||||
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key", color: .purple)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.AppearanceSettings(settings: settings)) {
|
||||
|
||||
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "eye", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.SearchSettings(settings: settings)) {
|
||||
|
||||
NavigationLink(destination: SearchSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "magnifyingglass", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.NotificationSettings(settings: settings)) {
|
||||
|
||||
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.ZapSettings(settings: settings)) {
|
||||
|
||||
NavigationLink(destination: ZapSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.TranslationSettings(settings: settings)) {
|
||||
|
||||
NavigationLink(destination: TranslationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
||||
Button(action: {
|
||||
@@ -115,11 +116,11 @@ struct ConfigView: View {
|
||||
guard let full_kp = state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard delete_text == DELETE_KEYWORD else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let ev = created_deleted_account_profile(keypair: full_kp)
|
||||
state.postbox.send(ev)
|
||||
notify(.logout, ())
|
||||
@@ -163,7 +164,7 @@ func handle_string_amount(new_value: String) -> Int? {
|
||||
guard let amt = NumberFormatter().number(from: filtered) as? Int else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import SwiftUI
|
||||
struct CreateAccountView: View {
|
||||
@StateObject var account: CreateAccountModel = CreateAccountModel()
|
||||
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
|
||||
var nav: NavigationCoordinator
|
||||
|
||||
@State var is_done: Bool = false
|
||||
|
||||
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
|
||||
return VStack(alignment: .leading, spacing: 10.0, content: content)
|
||||
@@ -24,6 +25,10 @@ struct CreateAccountView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
VStack {
|
||||
VStack(alignment: .center) {
|
||||
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
|
||||
@@ -58,10 +63,10 @@ struct CreateAccountView: View {
|
||||
.padding(.top, 10)
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.SaveKeys(account: account))
|
||||
self.is_done = true
|
||||
}) {
|
||||
HStack {
|
||||
Text("Create account now", comment: "Button to create account.")
|
||||
Text("Create account now", comment: "Button to create account.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
@@ -91,7 +96,7 @@ 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")
|
||||
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.")) {
|
||||
@@ -130,7 +135,7 @@ extension View {
|
||||
struct CreateAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let model = CreateAccountModel(real: "", nick: "jb55", about: "")
|
||||
return CreateAccountView(account: model, nav: .init())
|
||||
return CreateAccountView(account: model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +167,7 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
|
||||
Text(title)
|
||||
.bold()
|
||||
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)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ struct DMChatView: View, KeyboardReadable {
|
||||
|
||||
var Header: some View {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
|
||||
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
return NavigationLink(destination: profile_page) {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@ struct DirectMessagesView: View {
|
||||
@State var dm_type: DMType = .friend
|
||||
@ObservedObject var model: DirectMessagesModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
|
||||
func MainContent(requests: Bool) -> some View {
|
||||
ScrollView {
|
||||
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
|
||||
NavigationLink(destination: chat, isActive: $model.open_dm) {
|
||||
EmptyView()
|
||||
}
|
||||
LazyVStack(spacing: 0) {
|
||||
if model.dms.isEmpty, !model.loading {
|
||||
EmptyTimelineView()
|
||||
@@ -50,8 +54,7 @@ struct DirectMessagesView: View {
|
||||
if ok, let ev = model.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
.onTapGesture {
|
||||
self.model.set_active_dm_model(model)
|
||||
damus_state.nav.push(route: Route.DMChat(dms: self.model.active_model))
|
||||
self.model.open_dm_by_model(model)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -56,13 +56,18 @@ By using our Application, you signify your acceptance of this EULA. If you do no
|
||||
"""
|
||||
|
||||
struct EULAView: View {
|
||||
@State private var login = false
|
||||
@State var accepted = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var nav: NavigationCoordinator
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
NavigationLink(destination: LoginView(accepted: $accepted), isActive: $login) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Text(Markdown.parse(content: eula))
|
||||
.padding()
|
||||
}
|
||||
@@ -91,7 +96,8 @@ struct EULAView: View {
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.Login)
|
||||
accepted = true
|
||||
login.toggle()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")
|
||||
@@ -111,7 +117,7 @@ struct EULAView: View {
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
.navigationTitle(NSLocalizedString("EULA", comment: "Navigation title of view that shows the EULA, an acronym for End User License Agreement."))
|
||||
.navigationTitle("EULA")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
@@ -120,6 +126,6 @@ struct EULAView: View {
|
||||
|
||||
struct EULAView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EULAView(nav: .init())
|
||||
EULAView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,8 @@ struct EventView: View {
|
||||
}
|
||||
} else if event.known_kind == .zap {
|
||||
if let zap = damus.zaps.zaps[event.id] {
|
||||
ZapEvent(damus: damus, zap: zap, is_top_zap: options.contains(.top_zap))
|
||||
ZapEvent(damus: damus, zap: zap)
|
||||
} else {
|
||||
Text("Invalid Zap", comment: "Text indicating that a zap event is malformed and could not be displayed.")
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -72,7 +72,8 @@ struct BuilderEventView: View {
|
||||
if let event {
|
||||
let ev = event.get_inner_event(cache: damus.events) ?? event
|
||||
let thread = ThreadModel(event: ev, damus_state: damus)
|
||||
NavigationLink(value: Route.Thread(thread: thread)) {
|
||||
let dest = ThreadView(state: damus, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
EventView(damus: damus, event: event, options: .embedded)
|
||||
.padding([.top, .bottom], 8)
|
||||
}.buttonStyle(.plain)
|
||||
|
||||
@@ -37,7 +37,7 @@ struct EventProfile: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ struct MutedEventView: View {
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
|
||||
HStack {
|
||||
Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.")
|
||||
Text("Post from a user you've muted", comment: "Text to indicate that what is being shown is a post from a user who has been muted.")
|
||||
Spacer()
|
||||
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) {
|
||||
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been muted.")) {
|
||||
shown.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
|
||||
|
||||
let names: [String] = pubkeys.map {
|
||||
let prof = profiles.lookup(id: $0)
|
||||
return Profile.displayName(profile: prof, pubkey: $0).username.truncate(maxLength: 50)
|
||||
return Profile.displayName(profile: prof, pubkey: $0).username
|
||||
}
|
||||
|
||||
let uniqueNames = NSOrderedSet(array: names).array as! [String]
|
||||
|
||||
@@ -18,7 +18,6 @@ struct EventViewOptions: OptionSet {
|
||||
static let no_translate = EventViewOptions(rawValue: 1 << 6)
|
||||
static let small_pfp = EventViewOptions(rawValue: 1 << 7)
|
||||
static let nested = EventViewOptions(rawValue: 1 << 8)
|
||||
static let top_zap = EventViewOptions(rawValue: 1 << 9)
|
||||
|
||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||
}
|
||||
@@ -132,7 +131,7 @@ struct TextEvent: View {
|
||||
|
||||
func ProfileName(is_anon: Bool) -> some View {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
let pk = is_anon ? ANON_PUBKEY : pubkey
|
||||
let pk = is_anon ? "anon" : pubkey
|
||||
return EventProfileName(pubkey: pk, profile: profile, damus: damus, size: .normal)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,23 +10,13 @@ import SwiftUI
|
||||
struct ZapEvent: View {
|
||||
let damus: DamusState
|
||||
let zap: Zapping
|
||||
let is_top_zap: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
Image("zap.fill")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Text(verbatim: format_msats(zap.amount))
|
||||
Text("⚡️ \(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
|
||||
if is_top_zap {
|
||||
Text("Top Zap", comment: "Text indicating that this zap is the one with the highest amount of sats.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding([.top], 2)
|
||||
}
|
||||
.padding([.top], 2)
|
||||
|
||||
if zap.is_private {
|
||||
Image("lock")
|
||||
@@ -41,7 +31,7 @@ struct ZapEvent: View {
|
||||
}
|
||||
}
|
||||
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
.padding([.top], 1)
|
||||
}
|
||||
}
|
||||
@@ -51,18 +41,18 @@ struct ZapEvent: View {
|
||||
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
|
||||
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
|
||||
let test_zap_request = ZapRequest(ev: test_zap_request_ev)
|
||||
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: nil)
|
||||
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil)
|
||||
|
||||
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: .init(ev: test_event))
|
||||
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
|
||||
|
||||
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||
|
||||
struct ZapEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
ZapEvent(damus: test_damus_state(), zap: .zap(test_zap), is_top_zap: true)
|
||||
ZapEvent(damus: test_damus_state(), zap: .zap(test_zap))
|
||||
|
||||
ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap), is_top_zap: false)
|
||||
ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,16 @@ struct FollowUserView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
static let markdown = Markdown()
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
HStack {
|
||||
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: target.pubkey))
|
||||
}
|
||||
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
}
|
||||
@@ -26,27 +29,11 @@ struct FollowUserView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowersYouKnowView: View {
|
||||
let damus_state: DamusState
|
||||
let friended_followers: [String]
|
||||
@ObservedObject var followers: FollowersModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(friended_followers, id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("Followers You Know", comment: "Navigation bar title for view that shows who is following a user."))
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowersView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var followers: FollowersModel
|
||||
let whos: String
|
||||
|
||||
@EnvironmentObject var followers: FollowersModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -71,7 +58,7 @@ struct FollowingView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
let following: FollowingModel
|
||||
|
||||
let whos: String
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
||||
@@ -10,8 +10,7 @@ import Kingfisher
|
||||
|
||||
|
||||
struct ImageContainerView: View {
|
||||
let cache: EventCache
|
||||
let url: MediaUrl
|
||||
let url: URL?
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
@@ -27,7 +26,8 @@ struct ImageContainerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func Img(url: URL) -> some View {
|
||||
var body: some View {
|
||||
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note, disable_animation: disable_animation)
|
||||
.configure { view in
|
||||
@@ -40,23 +40,12 @@ struct ImageContainerView: View {
|
||||
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")!
|
||||
|
||||
struct ImageContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageContainerView(cache: test_damus_state().events, url: .image(test_image_url), disable_animation: false)
|
||||
ImageContainerView(url: test_image_url, disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
let cache: EventCache
|
||||
let urls: [MediaUrl]
|
||||
|
||||
let urls: [URL?]
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@@ -39,7 +39,7 @@ struct ImageView: View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(cache: cache, url: urls[index], disable_animation: disable_animation)
|
||||
ImageContainerView(url: urls[index], disable_animation: disable_animation)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
@@ -79,7 +79,6 @@ struct ImageView: View {
|
||||
|
||||
struct ImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageView(cache: test_damus_state().events, urls: [url], disable_animation: false)
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,13 @@ enum ParsedKey {
|
||||
}
|
||||
|
||||
struct LoginView: View {
|
||||
@State private var create_account = false
|
||||
@State var key: String = ""
|
||||
@State var is_pubkey: Bool = false
|
||||
@State var error: String? = nil
|
||||
@State private var credential_handler = CredentialHandler()
|
||||
var nav: NavigationCoordinator
|
||||
|
||||
@Binding var accepted: Bool
|
||||
|
||||
func get_error(parsed_key: ParsedKey?) -> String? {
|
||||
if self.error != nil {
|
||||
@@ -53,6 +55,12 @@ struct LoginView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
if accepted {
|
||||
NavigationLink(destination: CreateAccountView(), isActive: $create_account) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
SignInHeader()
|
||||
.padding(.top, 100)
|
||||
@@ -72,10 +80,9 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
if parsed?.is_pub ?? false {
|
||||
Text("This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.", comment: "Warning that the inputted account key is a public key and the result of what happens because of it.")
|
||||
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(Color.orange)
|
||||
.bold()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if let p = parsed {
|
||||
@@ -99,7 +106,7 @@ struct LoginView: View {
|
||||
.padding(.top, 10)
|
||||
}
|
||||
|
||||
CreateAccountPrompt(nav: nav)
|
||||
CreateAccountPrompt(create_account: $create_account)
|
||||
.padding(.top, 10)
|
||||
|
||||
Spacer()
|
||||
@@ -329,14 +336,14 @@ struct SignInEntry: View {
|
||||
}
|
||||
|
||||
struct CreateAccountPrompt: View {
|
||||
var nav: NavigationCoordinator
|
||||
@Binding var create_account: Bool
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("New to Nostr?", comment: "Ask the user if they are new to Nostr")
|
||||
Text("New to nostr?", comment: "Ask the user if they are new to nostr")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
|
||||
nav.push(route: Route.CreateAccount)
|
||||
create_account.toggle()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -350,8 +357,8 @@ struct LoginView_Previews: PreviewProvider {
|
||||
let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955"
|
||||
let bech32_pubkey = "KeyInput"
|
||||
Group {
|
||||
LoginView(key: pubkey, nav: .init())
|
||||
LoginView(key: bech32_pubkey, nav: .init())
|
||||
LoginView(key: pubkey, accepted: .constant(true))
|
||||
LoginView(key: bech32_pubkey, accepted: .constant(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct TabButton: View {
|
||||
let timeline: Timeline
|
||||
let img: String
|
||||
@Binding var selected: Timeline
|
||||
@ObservedObject var nstatus: NotificationStatusModel
|
||||
@Binding var new_events: NewEventsBits
|
||||
|
||||
let settings: UserSettingsStore
|
||||
let action: (Timeline) -> ()
|
||||
@@ -38,7 +38,7 @@ struct TabButton: View {
|
||||
ZStack(alignment: .center) {
|
||||
Tab
|
||||
|
||||
if show_indicator(timeline: timeline, current: nstatus.new_events, indicator_setting: settings.notification_indicators) {
|
||||
if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) {
|
||||
Circle()
|
||||
.size(CGSize(width: 8, height: 8))
|
||||
.frame(width: 10, height: 10, alignment: .topTrailing)
|
||||
@@ -53,7 +53,7 @@ struct TabButton: View {
|
||||
Button(action: {
|
||||
action(timeline)
|
||||
let bits = timeline_to_notification_bits(timeline, ev: nil)
|
||||
nstatus.new_events = NewEventsBits(rawValue: nstatus.new_events.rawValue & ~bits.rawValue)
|
||||
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
|
||||
}) {
|
||||
Image(selected != timeline ? img : "\(img).fill")
|
||||
.contentShape(Rectangle())
|
||||
@@ -65,7 +65,7 @@ struct TabButton: View {
|
||||
|
||||
|
||||
struct TabBar: View {
|
||||
var nstatus: NotificationStatusModel
|
||||
@Binding var new_events: NewEventsBits
|
||||
@Binding var selected: Timeline
|
||||
|
||||
let settings: UserSettingsStore
|
||||
@@ -75,10 +75,10 @@ struct TabBar: View {
|
||||
VStack {
|
||||
Divider()
|
||||
HStack {
|
||||
TabButton(timeline: .home, img: "home", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("1")
|
||||
TabButton(timeline: .dms, img: "messages", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("2")
|
||||
TabButton(timeline: .search, img: "search", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("3")
|
||||
TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("4")
|
||||
TabButton(timeline: .home, img: "home", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
|
||||
TabButton(timeline: .dms, img: "messages", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
|
||||
TabButton(timeline: .search, img: "search", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
|
||||
TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,11 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if show_images && artifacts.media.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
} else if !show_images && artifacts.media.count > 0 {
|
||||
if show_images && artifacts.images.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
} else if !show_images && artifacts.images.count > 0 {
|
||||
ZStack {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
Blur()
|
||||
.disabled(true)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
|
||||
case .pubkey:
|
||||
let pk = m.ref.ref_id
|
||||
let profile = profiles.lookup(id: pk)
|
||||
let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
|
||||
let disp = Profile.displayName(profile: profile, pubkey: pk).username
|
||||
var attributedString = AttributedString(stringLiteral: "@\(disp)")
|
||||
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
@@ -261,24 +261,13 @@ struct NoteArtifacts: Equatable {
|
||||
}
|
||||
|
||||
let content: CompatibleText
|
||||
let urls: [UrlType]
|
||||
let images: [URL]
|
||||
let invoices: [Invoice]
|
||||
|
||||
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 }
|
||||
}
|
||||
let links: [URL]
|
||||
|
||||
static func just_content(_ content: String) -> NoteArtifacts {
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
return NoteArtifacts(content: txt, urls: [], invoices: [])
|
||||
return NoteArtifacts(content: txt, images: [], invoices: [], links: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +304,8 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
||||
|
||||
func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
var invoices: [Invoice] = []
|
||||
var urls: [UrlType] = []
|
||||
var img_urls: [URL] = []
|
||||
var link_urls: [URL] = []
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({ $0.is_note_mention })
|
||||
@@ -333,14 +323,12 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
var trimmed = txt
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
if case .url(let u) = next, is_image_url(u) {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
@@ -357,112 +345,25 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
// Handle Image URLs
|
||||
if is_image_url(url) {
|
||||
// Append Image
|
||||
img_urls.append(url)
|
||||
return str
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
} else {
|
||||
link_urls.append(url)
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifacts(content: txt, urls: urls, invoices: invoices)
|
||||
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
||||
}
|
||||
|
||||
enum MediaUrl {
|
||||
case image(URL)
|
||||
case video(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UrlType {
|
||||
case media(MediaUrl)
|
||||
case link(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_video: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image:
|
||||
return nil
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_img: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video:
|
||||
return nil
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_link: URL? {
|
||||
switch self {
|
||||
case .media:
|
||||
return nil
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_media: MediaUrl? {
|
||||
switch self {
|
||||
case .media(let murl):
|
||||
return murl
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
func is_image_url(_ url: URL) -> Bool {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
|
||||
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)
|
||||
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp")
|
||||
return isUrl
|
||||
}
|
||||
|
||||
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
|
||||
|
||||
@@ -14,15 +14,6 @@ enum EventGroupType {
|
||||
case zap(ZapGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
|
||||
var is_note_zap: Bool {
|
||||
switch self {
|
||||
case .repost: return false
|
||||
case .reaction: return false
|
||||
case .zap: return true
|
||||
case .profile_zap: return false
|
||||
}
|
||||
}
|
||||
|
||||
var zap_group: ZapGroup? {
|
||||
switch self {
|
||||
case .profile_zap(let grp):
|
||||
@@ -51,7 +42,7 @@ enum EventGroupType {
|
||||
}
|
||||
|
||||
enum ReactingTo {
|
||||
case your_note
|
||||
case your_post
|
||||
case tagged_in
|
||||
case your_profile
|
||||
}
|
||||
@@ -62,7 +53,7 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
|
||||
}
|
||||
|
||||
if ev.pubkey == our_pubkey {
|
||||
return .your_note
|
||||
return .your_post
|
||||
}
|
||||
|
||||
return .tagged_in
|
||||
@@ -70,45 +61,22 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
|
||||
|
||||
func event_author_name(profiles: Profiles, pubkey: String) -> String {
|
||||
let alice_prof = profiles.lookup(id: pubkey)
|
||||
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username
|
||||
}
|
||||
|
||||
func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [String] {
|
||||
var seen = Set<String>()
|
||||
var sorted = [String]()
|
||||
|
||||
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
|
||||
if let zapgrp = group.zap_group {
|
||||
let zaps = zapgrp.zaps
|
||||
|
||||
for i in 0..<zaps.count {
|
||||
let zap = zapgrp.zaps[i]
|
||||
let pubkey: String
|
||||
|
||||
if zap.is_anon {
|
||||
pubkey = ANON_PUBKEY
|
||||
} else {
|
||||
pubkey = zap.request.ev.pubkey
|
||||
}
|
||||
|
||||
if !seen.contains(pubkey) {
|
||||
seen.insert(pubkey)
|
||||
sorted.append(pubkey)
|
||||
}
|
||||
let zap = zapgrp.zaps[ind]
|
||||
|
||||
if zap.is_anon {
|
||||
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
|
||||
}
|
||||
|
||||
return event_author_name(profiles: profiles, pubkey: zap.request.pubkey)
|
||||
} else {
|
||||
let events = group.events
|
||||
|
||||
for i in 0..<events.count {
|
||||
let ev = events[i]
|
||||
let pubkey = ev.pubkey
|
||||
if !seen.contains(pubkey) {
|
||||
seen.insert(pubkey)
|
||||
sorted.append(pubkey)
|
||||
}
|
||||
}
|
||||
let ev = group.events[ind]
|
||||
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
||||
}
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,9 +89,9 @@ func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [S
|
||||
"reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in
|
||||
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
|
||||
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
|
||||
"reacted_your_note_1" - returned when 1 reaction occurred to the current user's post
|
||||
"reacted_your_note_2" - returned when 2 reactions occurred to the current user's post
|
||||
"reacted_your_note_3" - returned when 3 or more reactions occurred to the current user's post
|
||||
"reacted_your_post_1" - returned when 1 reaction occurred to the current user's post
|
||||
"reacted_your_post_2" - returned when 2 reactions occurred to the current user's post
|
||||
"reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post
|
||||
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
|
||||
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
|
||||
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
|
||||
@@ -131,9 +99,9 @@ func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [S
|
||||
"reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in
|
||||
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
|
||||
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
|
||||
"reposted_your_note_1" - returned when 1 repost occurred to the current user's post
|
||||
"reposted_your_note_2" - returned when 2 reposts occurred to the current user's post
|
||||
"reposted_your_note_3" - returned when 3 or more reposts occurred to the current user's post
|
||||
"reposted_your_post_1" - returned when 1 repost occurred to the current user's post
|
||||
"reposted_your_post_2" - returned when 2 reposts occurred to the current user's post
|
||||
"reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post
|
||||
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
|
||||
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
|
||||
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
|
||||
@@ -141,36 +109,36 @@ func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [S
|
||||
"zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in
|
||||
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
|
||||
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
|
||||
"zapped_your_note_1" - returned when 1 zap occurred to the current user's post
|
||||
"zapped_your_note_2" - returned when 2 zaps occurred to the current user's post
|
||||
"zapped_your_note_3" - returned when 3 or more zaps occurred to the current user's post
|
||||
"zapped_your_post_1" - returned when 1 zap occurred to the current user's post
|
||||
"zapped_your_post_2" - returned when 2 zaps occurred to the current user's post
|
||||
"zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post
|
||||
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
|
||||
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
|
||||
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
|
||||
*/
|
||||
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, pubkeys: [String], locale: Locale? = nil) -> String {
|
||||
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String {
|
||||
if group.events.count == 0 {
|
||||
return "??"
|
||||
}
|
||||
|
||||
let verb = reacting_to_verb(group: group)
|
||||
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev)
|
||||
let localization_key = "\(verb)_\(reacting_to)_\(min(pubkeys.count, 3))"
|
||||
let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))"
|
||||
let format = localizedStringFormat(key: localization_key, locale: locale)
|
||||
|
||||
switch pubkeys.count {
|
||||
switch group.events.count {
|
||||
case 1:
|
||||
let display_name = event_author_name(profiles: profiles, pubkey: pubkeys[0])
|
||||
let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
|
||||
return String(format: format, locale: locale, display_name)
|
||||
case 2:
|
||||
let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0])
|
||||
let bob_name = event_author_name(profiles: profiles, pubkey: pubkeys[1])
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
|
||||
|
||||
return String(format: format, locale: locale, alice_name, bob_name)
|
||||
default:
|
||||
let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0])
|
||||
let count = pubkeys.count - 1
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let count = group.events.count - 1
|
||||
|
||||
return String(format: format, locale: locale, count, alice_name)
|
||||
}
|
||||
@@ -192,9 +160,9 @@ struct EventGroupView: View {
|
||||
let state: DamusState
|
||||
let event: NostrEvent?
|
||||
let group: EventGroupType
|
||||
|
||||
func GroupDescription(_ pubkeys: [String]) -> some View {
|
||||
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, pubkeys: pubkeys))")
|
||||
|
||||
var GroupDescription: some View {
|
||||
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))")
|
||||
}
|
||||
|
||||
func ZapIcon(_ zapgrp: ZapGroup) -> some View {
|
||||
@@ -234,15 +202,14 @@ struct EventGroupView: View {
|
||||
.frame(width: PFP_SIZE + 10)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let unique_pubkeys = event_group_unique_pubkeys(profiles: state.profiles, group: group)
|
||||
|
||||
ProfilePicturesView(state: state, pubkeys: unique_pubkeys)
|
||||
ProfilePicturesView(state: state, pubkeys: group.events.map { $0.pubkey })
|
||||
|
||||
if let event {
|
||||
let thread = ThreadModel(event: event, damus_state: state)
|
||||
NavigationLink(value: Route.Thread(thread: thread)) {
|
||||
let dest = ThreadView(state: state, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
VStack(alignment: .leading) {
|
||||
GroupDescription(unique_pubkeys)
|
||||
GroupDescription
|
||||
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])
|
||||
.padding([.top], 1)
|
||||
.padding([.trailing])
|
||||
@@ -251,7 +218,7 @@ struct EventGroupView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
GroupDescription(unique_pubkeys)
|
||||
GroupDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ struct NotificationItemView: View {
|
||||
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
|
||||
|
||||
case .reply(let ev):
|
||||
NavigationLink(value: Route.Thread(thread: ThreadModel(event: ev, damus_state: state))) {
|
||||
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
|
||||
EventView(damus: state, event: ev, options: options)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -92,7 +92,7 @@ struct NotificationsView: View {
|
||||
|
||||
var mystery: some View {
|
||||
VStack(spacing: 20) {
|
||||
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("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("You are dreaming...", comment: "Text telling the user that they are dreaming.")
|
||||
}
|
||||
.id("what")
|
||||
|
||||
@@ -10,13 +10,20 @@ import SwiftUI
|
||||
struct ProfilePicturesView: View {
|
||||
let state: DamusState
|
||||
let pubkeys: [String]
|
||||
|
||||
@State var nav_target: String? = nil
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
HStack {
|
||||
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in
|
||||
ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
||||
.onTapGesture {
|
||||
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||
nav_target = pubkey
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,21 @@ struct ParticipantsView: View {
|
||||
ForEach(originalReferences.pRefs) { participant in
|
||||
let pubkey = participant.id
|
||||
HStack {
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
Text(about_string)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("check-circle.fill")
|
||||
.font(.system(size: 30))
|
||||
|
||||
+47
-90
@@ -13,21 +13,16 @@ enum NostrPostResult {
|
||||
case cancel
|
||||
}
|
||||
|
||||
let POST_PLACEHOLDER = NSLocalizedString("Type your note here...", comment: "Text box prompt to ask user to type their note.")
|
||||
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 PostTarget {
|
||||
case none
|
||||
case user(String)
|
||||
}
|
||||
|
||||
enum PostAction {
|
||||
case replying_to(NostrEvent)
|
||||
case quoting(NostrEvent)
|
||||
case posting(PostTarget)
|
||||
case posting
|
||||
|
||||
var ev: NostrEvent? {
|
||||
switch self {
|
||||
@@ -84,27 +79,11 @@ struct PostView: View {
|
||||
|
||||
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
||||
if let link = attributes[.link] as? String {
|
||||
let normalized_link: String
|
||||
if link.hasPrefix("damus:nostr:") {
|
||||
// Replace damus:nostr: URI prefix with nostr: since the former is for internal navigation and not meant to be posted.
|
||||
normalized_link = String(link.dropFirst(6))
|
||||
} else {
|
||||
normalized_link = link
|
||||
}
|
||||
|
||||
// Add zero-width space in case text preceding the mention is not a whitespace.
|
||||
// In the case where the character preceding the mention is a whitespace, the added zero-width space will be stripped out.
|
||||
post.replaceCharacters(in: range, with: "\u{200B}\(normalized_link)\u{200B}")
|
||||
post.replaceCharacters(in: range, with: link)
|
||||
}
|
||||
}
|
||||
|
||||
var content = self.post.string
|
||||
// If two zero-width spaces are next to each other, normalize it to just one zero-width space.
|
||||
.replacingOccurrences(of: "\u{200B}\u{200B}", with: "\u{200B}")
|
||||
// If zero-width space is next to an actual whitespace, remove the zero-width space.
|
||||
.replacingOccurrences(of: " \u{200B}", with: " ")
|
||||
.replacingOccurrences(of: "\u{200B} ", with: " ")
|
||||
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
|
||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
|
||||
|
||||
@@ -130,14 +109,6 @@ struct PostView: View {
|
||||
var is_post_empty: Bool {
|
||||
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
|
||||
}
|
||||
|
||||
var uploading_disabled: Bool {
|
||||
return image_upload.progress != nil
|
||||
}
|
||||
|
||||
var posting_disabled: Bool {
|
||||
return is_post_empty || uploading_disabled
|
||||
}
|
||||
|
||||
var ImageButton: some View {
|
||||
Button(action: {
|
||||
@@ -162,7 +133,7 @@ struct PostView: View {
|
||||
ImageButton
|
||||
CameraButton
|
||||
}
|
||||
.disabled(uploading_disabled)
|
||||
.disabled(image_upload.progress != nil)
|
||||
}
|
||||
|
||||
var PostButton: some View {
|
||||
@@ -173,29 +144,18 @@ struct PostView: View {
|
||||
self.send_post()
|
||||
}
|
||||
}
|
||||
.disabled(posting_disabled)
|
||||
.disabled(is_post_empty)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.frame(width: 80, height: 30)
|
||||
.foregroundColor(.white)
|
||||
.background(LINEAR_GRADIENT)
|
||||
.opacity(posting_disabled ? 0.5 : 1.0)
|
||||
.opacity(is_post_empty ? 0.5 : 1.0)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
func isEmpty() -> Bool {
|
||||
return self.uploadedMedias.count == 0 &&
|
||||
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines) ==
|
||||
initialString().mutableString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
func initialString() -> NSMutableAttributedString {
|
||||
guard case .posting(let target) = action,
|
||||
case .user(let pubkey) = target else {
|
||||
return .init(string: "")
|
||||
}
|
||||
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
return user_tag_attr_string(profile: profile, pubkey: pubkey)
|
||||
var isEmpty: Bool {
|
||||
self.uploadedMedias.count == 0 &&
|
||||
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
func clear_draft() {
|
||||
@@ -210,17 +170,15 @@ struct PostView: View {
|
||||
|
||||
}
|
||||
|
||||
func load_draft() -> Bool {
|
||||
func load_draft() {
|
||||
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
|
||||
self.post = NSMutableAttributedString("")
|
||||
self.uploadedMedias = []
|
||||
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
self.uploadedMedias = draft.media
|
||||
self.post = draft.content
|
||||
return true
|
||||
}
|
||||
|
||||
func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
|
||||
@@ -249,25 +207,29 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var TextEntry: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
||||
focusWordAttributes = (word, range)
|
||||
self.newCursorIndex = nil
|
||||
})
|
||||
func TextEntry(scrollViewGeometry: GeometryProxy) -> some View {
|
||||
GeometryReader { (geometry: GeometryProxy) in
|
||||
ZStack(alignment: .topLeading) {
|
||||
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
||||
focusWordAttributes = (word, range)
|
||||
self.newCursorIndex = nil
|
||||
})
|
||||
.environmentObject(tagModel)
|
||||
.frame(maxHeight: scrollViewGeometry.size.height)
|
||||
.position(x: geometry.frame(in: .local).midX, y: scrollViewGeometry.frame(in: .local).midY)
|
||||
.focused($focus)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.onChange(of: post) { p in
|
||||
post_changed(post: p, media: uploadedMedias)
|
||||
}
|
||||
|
||||
if post.string.isEmpty {
|
||||
Text(POST_PLACEHOLDER)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 4)
|
||||
.foregroundColor(Color(uiColor: .placeholderText))
|
||||
.allowsHitTesting(false)
|
||||
|
||||
if post.string.isEmpty {
|
||||
Text(POST_PLACEHOLDER)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 4)
|
||||
.foregroundColor(Color(uiColor: .placeholderText))
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,12 +301,12 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func Editor(deviceSize: GeometryProxy) -> some View {
|
||||
func Editor(deviceSize: GeometryProxy, scrollViewGeometry: GeometryProxy) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top) {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
|
||||
TextEntry
|
||||
TextEntry(scrollViewGeometry: scrollViewGeometry)
|
||||
}
|
||||
.frame(height: deviceSize.size.height * multiply_factor)
|
||||
.id("post")
|
||||
@@ -361,11 +323,6 @@ struct PostView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
func fill_target_content(target: PostTarget) {
|
||||
self.post = initialString()
|
||||
self.tagModel.diff = post.string.count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { (deviceSize: GeometryProxy) in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -374,16 +331,18 @@ struct PostView: View {
|
||||
TopBar
|
||||
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
if case .replying_to(let replying_to) = self.action {
|
||||
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
|
||||
GeometryReader { (geometry: GeometryProxy) in
|
||||
ScrollView {
|
||||
if case .replying_to(let replying_to) = self.action {
|
||||
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
|
||||
}
|
||||
|
||||
Editor(deviceSize: deviceSize, scrollViewGeometry: geometry)
|
||||
}
|
||||
.frame(maxHeight: searching == nil ? .infinity : 70)
|
||||
.onAppear {
|
||||
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
|
||||
}
|
||||
|
||||
Editor(deviceSize: deviceSize)
|
||||
}
|
||||
.frame(maxHeight: searching == nil ? .infinity : 70)
|
||||
.onAppear {
|
||||
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +394,7 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
let loaded_draft = load_draft()
|
||||
load_draft()
|
||||
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
@@ -444,10 +403,8 @@ struct PostView: View {
|
||||
case .quoting(let quoting):
|
||||
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
||||
originalReferences = references
|
||||
case .posting(let target):
|
||||
guard !loaded_draft else { break }
|
||||
|
||||
fill_target_content(target: target)
|
||||
case .posting:
|
||||
break
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
@@ -455,7 +412,7 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if isEmpty() {
|
||||
if isEmpty {
|
||||
clear_draft()
|
||||
}
|
||||
}
|
||||
@@ -495,7 +452,7 @@ func get_searching_string(_ word: String?) -> String? {
|
||||
|
||||
struct PostView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PostView(action: .posting(.none), damus_state: test_damus_state())
|
||||
PostView(action: .posting, damus_state: test_damus_state())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ struct UserSearch: View {
|
||||
guard let pk = bech32_pubkey(user.pubkey) else {
|
||||
return
|
||||
}
|
||||
let tagAttributedString = user_tag_attr_string(profile: user.profile, pubkey: pk)
|
||||
let tagAttributedString = createUserTag(for: user, with: pk)
|
||||
appendUserTag(withTag: tagAttributedString)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,26 @@ struct UserSearch: View {
|
||||
newCursorIndex = wordRange.location + tagAttributedString.string.count
|
||||
}
|
||||
|
||||
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
|
||||
let name = Profile.displayName(profile: user.profile, pubkey: pk).username
|
||||
let tagString = "@\(name)\u{200B} "
|
||||
|
||||
let tagAttributedString = NSMutableAttributedString(string: tagString,
|
||||
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
|
||||
NSAttributedString.Key.link: "@\(pk)"])
|
||||
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
|
||||
return tagAttributedString
|
||||
}
|
||||
|
||||
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) {
|
||||
let mutableString = NSMutableAttributedString()
|
||||
mutableString.append(post)
|
||||
mutableString.append(tagAttributedString)
|
||||
post = mutableString
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
@@ -148,18 +168,3 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func user_tag_attr_string(profile: Profile?, pubkey: String) -> NSMutableAttributedString {
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
let name = display_name.username.truncate(maxLength: 50)
|
||||
let tagString = "@\(name)\u{200B} "
|
||||
|
||||
let tagAttributedString = NSMutableAttributedString(string: tagString,
|
||||
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
|
||||
NSAttributedString.Key.link: "nostr:\(pubkey)"])
|
||||
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
|
||||
return tagAttributedString
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// AboutView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-18.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
let state: DamusState
|
||||
let about: String
|
||||
let max_about_length = 280
|
||||
@State var show_full_about: Bool = false
|
||||
@State private var about_string: AttributedString? = nil
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let about_string {
|
||||
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
|
||||
SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline)
|
||||
|
||||
if truncated_about != nil {
|
||||
if show_full_about {
|
||||
Button(NSLocalizedString("Show less", comment: "Button to show less of a long profile description.")) {
|
||||
show_full_about = false
|
||||
}
|
||||
.font(.footnote)
|
||||
} else {
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show more of a long profile description.")) {
|
||||
show_full_about = true
|
||||
}
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(verbatim: "")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
about_string = render_blocks(blocks: blocks, profiles: state.profiles).content.attributed
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#Preview {
|
||||
AboutView()
|
||||
}
|
||||
*/
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// CondensedProfilePicturesView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/19/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CondensedProfilePicturesView: View {
|
||||
let state: DamusState
|
||||
let pubkeys: [String]
|
||||
let maxPictures: Int
|
||||
|
||||
init(state: DamusState, pubkeys: [String], maxPictures: Int) {
|
||||
self.state = state
|
||||
self.pubkeys = pubkeys
|
||||
self.maxPictures = min(maxPictures, pubkeys.count)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Using ZStack to make profile pictures floating and stacked on top of each other.
|
||||
ZStack {
|
||||
ForEach((0..<maxPictures).reversed(), id: \.self) { index in
|
||||
ProfilePicView(pubkey: pubkeys[index], size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
||||
.offset(x: CGFloat(index) * 20)
|
||||
}
|
||||
}
|
||||
// Padding is needed so that other components drawn adjacent to this view don't get drawn on top.
|
||||
.padding(.trailing, CGFloat((maxPictures - 1) * 20))
|
||||
}
|
||||
}
|
||||
|
||||
struct CondensedProfilePicturesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CondensedProfilePicturesView(state: test_damus_state(), pubkeys: ["a", "b", "c", "d"], maxPictures: 3)
|
||||
}
|
||||
}
|
||||
@@ -103,15 +103,13 @@ struct EditMetadataView: View {
|
||||
TopSection
|
||||
Form {
|
||||
Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) {
|
||||
let display_name_placeholder = "Satoshi Nakamoto"
|
||||
TextField(display_name_placeholder, text: $display_name)
|
||||
TextField("Satoshi Nakamoto", text: $display_name)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) {
|
||||
let username_placeholder = "satoshi"
|
||||
TextField(username_placeholder, text: $name)
|
||||
TextField("satoshi", text: $name)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ struct MaybeAnonPfpView: View {
|
||||
.font(.largeTitle)
|
||||
.frame(width: size, height: size)
|
||||
} else {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
|
||||
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,6 @@ struct MaybeAnonPfpView: View {
|
||||
|
||||
struct MaybeAnonPfpView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: ANON_PUBKEY, size: PFP_SIZE)
|
||||
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon", size: PFP_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ struct ProfileName: View {
|
||||
}
|
||||
|
||||
var name_choice: String {
|
||||
return prefix == "@" ? current_display_name.username.truncate(maxLength: 50) : current_display_name.display_name.truncate(maxLength: 50)
|
||||
return prefix == "@" ? current_display_name.username : current_display_name.display_name
|
||||
}
|
||||
|
||||
var onlyzapper: Bool {
|
||||
|
||||
@@ -92,7 +92,6 @@ struct InnerProfilePicView: View {
|
||||
var Placeholder: some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||
.padding(2)
|
||||
}
|
||||
|
||||
@@ -46,38 +46,13 @@ func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
func followedByString(_ friend_intersection: [String], profiles: Profiles, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let names: [String] = friend_intersection.prefix(3).map {
|
||||
let profile = profiles.lookup(id: $0)
|
||||
return Profile.displayName(profile: profile, pubkey: $0).username.truncate(maxLength: 20)
|
||||
}
|
||||
|
||||
switch friend_intersection.count {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
let format = NSLocalizedString("Followed by %@", bundle: bundle, comment: "Text to indicate that the user is followed by one of our follows.")
|
||||
return String(format: format, locale: locale, names[0])
|
||||
case 2:
|
||||
let format = NSLocalizedString("Followed by %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by two of our follows.")
|
||||
return String(format: format, locale: locale, names[0], names[1])
|
||||
case 3:
|
||||
let format = NSLocalizedString("Followed by %@, %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by three of our follows.")
|
||||
return String(format: format, locale: locale, names[0], names[1], names[2])
|
||||
default:
|
||||
let format = localizedStringFormat(key: "followed_by_three_and_others", locale: locale)
|
||||
return String(format: format, locale: locale, friend_intersection.count - 3, names[0], names[1], names[2])
|
||||
}
|
||||
}
|
||||
|
||||
struct EditButton: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(value: Route.EditMetadata) {
|
||||
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
|
||||
Text("Edit", comment: "Button to edit user's profile.")
|
||||
.frame(height: 30)
|
||||
.padding(.horizontal,25)
|
||||
@@ -92,11 +67,11 @@ struct EditButton: View {
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fillColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
|
||||
|
||||
func borderColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
@@ -104,11 +79,11 @@ struct EditButton: View {
|
||||
|
||||
struct VisualEffectView: UIViewRepresentable {
|
||||
var effect: UIVisualEffect?
|
||||
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
|
||||
UIVisualEffectView()
|
||||
}
|
||||
|
||||
|
||||
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
|
||||
uiView.effect = effect
|
||||
}
|
||||
@@ -120,52 +95,52 @@ struct ProfileView: View {
|
||||
let bannerHeight: CGFloat = 150.0
|
||||
|
||||
static let markdown = Markdown()
|
||||
|
||||
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var is_zoomed: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_qr_code: Bool = false
|
||||
@State var action_sheet_presented: Bool = false
|
||||
@State var filter_state : FilterState = .posts
|
||||
@State var yOffset: CGFloat = 0
|
||||
|
||||
|
||||
@StateObject var profile: ProfileModel
|
||||
@StateObject var followers: FollowersModel
|
||||
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
|
||||
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
|
||||
self.damus_state = damus_state
|
||||
self._profile = StateObject(wrappedValue: profile)
|
||||
self._followers = StateObject(wrappedValue: followers)
|
||||
}
|
||||
|
||||
|
||||
init(damus_state: DamusState, pubkey: String) {
|
||||
self.damus_state = damus_state
|
||||
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
|
||||
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
|
||||
}
|
||||
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
|
||||
func imageBorderColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.white : DamusColors.black
|
||||
}
|
||||
|
||||
|
||||
func bannerBlurViewOpacity() -> Double {
|
||||
let progress = -(yOffset + navbarHeight) / 100
|
||||
return Double(-yOffset > navbarHeight ? progress : 0)
|
||||
}
|
||||
|
||||
|
||||
var bannerSection: some View {
|
||||
GeometryReader { proxy -> AnyView in
|
||||
|
||||
|
||||
let minY = proxy.frame(in: .global).minY
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.yOffset = minY
|
||||
}
|
||||
|
||||
|
||||
return AnyView(
|
||||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
@@ -173,10 +148,10 @@ struct ProfileView: View {
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
|
||||
.clipped()
|
||||
|
||||
|
||||
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
|
||||
}
|
||||
|
||||
|
||||
Divider().opacity(bannerBlurViewOpacity())
|
||||
}
|
||||
.frame(height: minY > 0 ? bannerHeight + minY : nil)
|
||||
@@ -187,11 +162,11 @@ struct ProfileView: View {
|
||||
.frame(height: bannerHeight)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
|
||||
var navbarHeight: CGFloat {
|
||||
return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
func navImage(img: String) -> some View {
|
||||
Image(img)
|
||||
@@ -199,7 +174,7 @@ struct ProfileView: View {
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
|
||||
var navBackButton: some View {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
@@ -207,7 +182,7 @@ struct ProfileView: View {
|
||||
navImage(img: "chevron-left")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var navActionSheetButton: some View {
|
||||
Button(action: {
|
||||
action_sheet_presented = true
|
||||
@@ -218,7 +193,7 @@ struct ProfileView: View {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
|
||||
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
|
||||
show_qr_code = true
|
||||
}
|
||||
@@ -238,7 +213,7 @@ struct ProfileView: View {
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: profile.pubkey) else {
|
||||
return
|
||||
}
|
||||
@@ -254,7 +229,7 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var customNavbar: some View {
|
||||
HStack {
|
||||
navBackButton
|
||||
@@ -265,11 +240,15 @@ struct ProfileView: View {
|
||||
.padding(.horizontal)
|
||||
.accentColor(DamusColors.white)
|
||||
}
|
||||
|
||||
|
||||
func lnButton(lnurl: String, profile: Profile) -> some View {
|
||||
let button_img = profile.reactions == false ? "zap.fill" : "zap"
|
||||
return Button(action: {
|
||||
present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl))
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
|
||||
}
|
||||
}) {
|
||||
Image(button_img)
|
||||
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
|
||||
@@ -278,7 +257,7 @@ struct ProfileView: View {
|
||||
if profile.reactions == false {
|
||||
Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.")
|
||||
}
|
||||
|
||||
|
||||
if let addr = profile.lud16 {
|
||||
Button {
|
||||
UIPasteboard.general.string = addr
|
||||
@@ -293,30 +272,34 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.cornerRadius(24)
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
|
||||
}
|
||||
}
|
||||
|
||||
var dmButton: some View {
|
||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||
return NavigationLink(value: Route.DMChat(dms: dm_model)) {
|
||||
let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
|
||||
return NavigationLink(destination: dmview) {
|
||||
Image("messages")
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func actionSection(profile_data: Profile?) -> some View {
|
||||
return Group {
|
||||
|
||||
|
||||
if let profile = profile_data {
|
||||
if let lnurl = profile.lnurl, lnurl != "" {
|
||||
lnButton(lnurl: lnurl, profile: profile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dmButton
|
||||
|
||||
|
||||
if profile.pubkey != damus_state.pubkey {
|
||||
FollowButtonView(
|
||||
target: profile.get_follow_target(),
|
||||
@@ -324,26 +307,26 @@ struct ProfileView: View {
|
||||
follow_state: damus_state.contacts.follow_state(profile.pubkey)
|
||||
)
|
||||
} else if damus_state.keypair.privkey != nil {
|
||||
NavigationLink(value: Route.EditMetadata) {
|
||||
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
|
||||
EditButton(damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func pfpOffset() -> CGFloat {
|
||||
let progress = -yOffset / navbarHeight
|
||||
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
|
||||
return offset > 0 ? offset : 0
|
||||
}
|
||||
|
||||
|
||||
func pfpScale() -> CGFloat {
|
||||
let progress = -yOffset / navbarHeight
|
||||
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
|
||||
return scale < 1 ? scale : 1
|
||||
}
|
||||
|
||||
|
||||
func nameSection(profile_data: Profile?) -> some View {
|
||||
return Group {
|
||||
HStack(alignment: .center) {
|
||||
@@ -357,17 +340,17 @@ struct ProfileView: View {
|
||||
.fullScreenCover(isPresented: $is_zoomed) {
|
||||
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
actionSection(profile_data: profile_data)
|
||||
}
|
||||
|
||||
|
||||
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
|
||||
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var followersCount: some View {
|
||||
HStack {
|
||||
if followers.count == nil {
|
||||
@@ -384,26 +367,31 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var aboutSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
|
||||
|
||||
|
||||
nameSection(profile_data: profile_data)
|
||||
|
||||
if let about = profile_data?.about {
|
||||
AboutView(state: damus_state, about: about)
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
SelectableText(attributedString: about_string, size: .subheadline)
|
||||
} else {
|
||||
Text(verbatim: "")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
|
||||
if let url = profile_data?.website_url {
|
||||
WebsiteLink(url: url)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
if let contact = profile.contacts {
|
||||
let contacts = contact.referenced_pubkeys.map { $0.ref_id }
|
||||
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
|
||||
NavigationLink(value: Route.Following(following: following_model)) {
|
||||
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
|
||||
HStack {
|
||||
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
|
||||
Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
|
||||
@@ -411,9 +399,10 @@ struct ProfileView: View {
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey)
|
||||
.environmentObject(followers)
|
||||
if followers.contacts != nil {
|
||||
NavigationLink(value: Route.Followers(followers: followers)) {
|
||||
NavigationLink(destination: fview) {
|
||||
followersCount
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
@@ -425,108 +414,84 @@ struct ProfileView: View {
|
||||
followers.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let relays = profile.relays {
|
||||
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
||||
let noun_text = Text(verbatim: relaysCountString(relays.keys.count)).font(.subheadline).foregroundColor(.gray)
|
||||
let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
||||
NavigationLink(value: Route.RelayConfig) {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state)) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
} else {
|
||||
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
|
||||
NavigationLink(destination: UserRelaysView(state: damus_state, relays: Array(relays.keys).sorted())) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if profile.pubkey != damus_state.pubkey {
|
||||
let friended_followers = damus_state.contacts.get_friended_followers(profile.pubkey)
|
||||
if !friended_followers.isEmpty {
|
||||
Spacer()
|
||||
|
||||
NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) {
|
||||
HStack {
|
||||
CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3)
|
||||
Text(followedByString(friended_followers, profiles: damus_state.profiles))
|
||||
.font(.subheadline).foregroundColor(.gray)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView(.vertical) {
|
||||
VStack(spacing: 0) {
|
||||
bannerSection
|
||||
.zIndex(1)
|
||||
ScrollView(.vertical) {
|
||||
VStack(spacing: 0) {
|
||||
bannerSection
|
||||
.zIndex(1)
|
||||
|
||||
VStack() {
|
||||
aboutSection
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Posts", comment: "Label for filter for seeing only your posts (instead of posts and replies).").tag(FilterState.posts)
|
||||
Text("Posts & Replies", comment: "Label for filter for seeing your posts and replies (instead of only your posts).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
|
||||
VStack() {
|
||||
aboutSection
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
|
||||
Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
|
||||
if filter_state == FilterState.posts {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
|
||||
}
|
||||
if filter_state == FilterState.posts_and_replies {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
|
||||
}
|
||||
if filter_state == FilterState.posts {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
|
||||
}
|
||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
.overlay(customNavbar, alignment: .top)
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.onAppear() {
|
||||
profile.subscribe()
|
||||
//followers.subscribe()
|
||||
}
|
||||
.onDisappear {
|
||||
profile.unsubscribe()
|
||||
followers.unsubscribe()
|
||||
// our profilemodel needs a bit more help
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet) {
|
||||
if let npub = bech32_pubkey(profile.pubkey) {
|
||||
if let url = URL(string: "https://damus.io/" + npub) {
|
||||
ShareSheet(activityItems: [url])
|
||||
if filter_state == FilterState.posts_and_replies {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
||||
}
|
||||
.fullScreenCover(isPresented: $show_qr_code) {
|
||||
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
|
||||
}
|
||||
|
||||
if damus_state.is_privkey_user {
|
||||
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
|
||||
notify(.compose, PostAction.posting(.user(profile.pubkey)))
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
.overlay(customNavbar, alignment: .top)
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.onAppear() {
|
||||
profile.subscribe()
|
||||
//followers.subscribe()
|
||||
}
|
||||
.onDisappear {
|
||||
profile.unsubscribe()
|
||||
followers.unsubscribe()
|
||||
// our profilemodel needs a bit more help
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet) {
|
||||
if let npub = bech32_pubkey(profile.pubkey) {
|
||||
if let url = URL(string: "https://damus.io/" + npub) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $show_qr_code) {
|
||||
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +505,7 @@ struct ProfileView_Previews: PreviewProvider {
|
||||
func test_damus_state() -> DamusState {
|
||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let damus = DamusState.empty
|
||||
|
||||
|
||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)
|
||||
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
|
||||
damus.profiles.add(id: pubkey, profile: tsprof)
|
||||
@@ -549,15 +514,15 @@ func test_damus_state() -> DamusState {
|
||||
|
||||
struct KeyView: View {
|
||||
let pubkey: String
|
||||
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
|
||||
@State private var isCopied = false
|
||||
|
||||
|
||||
func keyColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
|
||||
|
||||
private func copyPubkey(_ pubkey: String) {
|
||||
UIPasteboard.general.string = pubkey
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
@@ -570,10 +535,10 @@ struct KeyView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
let bech32 = bech32_pubkey(pubkey) ?? pubkey
|
||||
|
||||
|
||||
HStack {
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
|
||||
.font(.footnote)
|
||||
@@ -581,7 +546,7 @@ struct KeyView: View {
|
||||
.padding(5)
|
||||
.padding([.leading, .trailing], 5)
|
||||
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
|
||||
|
||||
|
||||
if isCopied != true {
|
||||
Button {
|
||||
copyPubkey(bech32)
|
||||
|
||||
+71
-242
@@ -8,53 +8,11 @@
|
||||
import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct ProfileScanResult: Equatable {
|
||||
let pubkey: String
|
||||
|
||||
init(hex: String) {
|
||||
self.pubkey = hex
|
||||
}
|
||||
|
||||
init?(string: String) {
|
||||
var str = string
|
||||
guard str.count != 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if str.hasPrefix("nostr:") {
|
||||
str.removeFirst("nostr:".count)
|
||||
}
|
||||
|
||||
if let _ = hex_decode(str), str.count == 64 {
|
||||
self = .init(hex: str)
|
||||
return
|
||||
}
|
||||
|
||||
if str.starts(with: "npub"), let b32 = try? bech32_decode(str) {
|
||||
let hex = hex_encode(b32.data)
|
||||
self = .init(hex: hex)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct QRCodeView: View {
|
||||
let damus_state: DamusState
|
||||
@State var pubkey: String
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var selectedTab = 0
|
||||
@State var scanResult: ProfileScanResult? = nil
|
||||
@State var profile: Profile? = nil
|
||||
@State var error: String? = nil
|
||||
@State private var outerTrimEnd: CGFloat = 0
|
||||
|
||||
var animationDuration: Double = 0.5
|
||||
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
|
||||
var maybe_key: String? {
|
||||
guard let key = bech32_pubkey(pubkey) else {
|
||||
@@ -64,215 +22,87 @@ struct QRCodeView: View {
|
||||
return key
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func navImage(systemImage: String) -> some View {
|
||||
Image(systemName: systemImage)
|
||||
.frame(width: 33, height: 33)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
var navBackButton: some View {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
navImage(systemImage: "chevron.left")
|
||||
}
|
||||
}
|
||||
|
||||
var customNavbar: some View {
|
||||
HStack {
|
||||
navBackButton
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.padding(.horizontal)
|
||||
.accentColor(DamusColors.white)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DamusGradient()
|
||||
ZStack(alignment: .center) {
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
DamusGradient()
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Image("close")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
TabView(selection: $selectedTab) {
|
||||
QRView
|
||||
.tag(0)
|
||||
if pubkey == damus_state.pubkey {
|
||||
QRCameraView()
|
||||
.tag(1)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.onAppear {
|
||||
UIScrollView.appearance().isScrollEnabled = false
|
||||
}
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged { _ in }
|
||||
)
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
.overlay(customNavbar, alignment: .top)
|
||||
}
|
||||
|
||||
var QRView: some View {
|
||||
VStack(alignment: .center) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
|
||||
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
|
||||
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
.padding(.top, 50)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 60))
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
}
|
||||
if let name = profile?.name {
|
||||
Text("@" + name)
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let key = maybe_key {
|
||||
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
|
||||
.interpolation(.none)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 300, height: 300)
|
||||
.cornerRadius(10)
|
||||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.white, lineWidth: 5.0))
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
.padding(.top)
|
||||
|
||||
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
|
||||
.font(.system(size: 18, weight: .ultraLight))
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
selectedTab = 1
|
||||
}) {
|
||||
HStack {
|
||||
Text("Scan Code", comment: "Button to switch to scan QR Code page.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(50)
|
||||
}
|
||||
}
|
||||
|
||||
func QRCameraView() -> some View {
|
||||
return VStack(alignment: .center) {
|
||||
Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.")
|
||||
.padding(.top, 50)
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
|
||||
Spacer()
|
||||
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
handleProfileScan(success.string)
|
||||
case .failure(let failure):
|
||||
self.error = failure.localizedDescription
|
||||
}
|
||||
}
|
||||
.scaledToFit()
|
||||
.frame(width: 300, height: 300)
|
||||
.cornerRadius(10)
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
|
||||
.rotationEffect(.degrees(-90)))
|
||||
.shadow(radius: 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
selectedTab = 0
|
||||
}) {
|
||||
HStack {
|
||||
Text("View QR Code", comment: "Button to switch to view users QR Code")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame( maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(50)
|
||||
}
|
||||
}
|
||||
|
||||
func handleProfileScan(_ scanned_str: String) {
|
||||
guard let result = ProfileScanResult(string: scanned_str) else {
|
||||
self.error = "Invalid profile QR"
|
||||
return
|
||||
}
|
||||
|
||||
self.error = nil
|
||||
|
||||
guard result != self.scanResult else {
|
||||
return
|
||||
}
|
||||
|
||||
generator.impactOccurred()
|
||||
cameraAnimate {
|
||||
scanResult = result
|
||||
|
||||
find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in
|
||||
guard let res else {
|
||||
error = "Profile not found"
|
||||
return
|
||||
VStack(alignment: .center) {
|
||||
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
|
||||
if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
|
||||
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
.padding(.top, 50)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(DamusColors.white)
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
switch res {
|
||||
case .invalid_profile:
|
||||
error = "Profile was found but was corrupt."
|
||||
|
||||
case .profile:
|
||||
show_profile_after_delay()
|
||||
|
||||
case .event:
|
||||
print("invalid search result")
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
}
|
||||
if let name = profile?.name {
|
||||
Text("@" + name)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let key = maybe_key {
|
||||
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
|
||||
.interpolation(.none)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 200)
|
||||
.padding()
|
||||
.cornerRadius(10)
|
||||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.white, lineWidth: 1))
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if (pubkey == damus_state.pubkey) {
|
||||
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
|
||||
.foregroundColor(DamusColors.white)
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
.padding(.top)
|
||||
} else {
|
||||
Text("Follow them on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user (someone else).")
|
||||
.foregroundColor(DamusColors.white)
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
|
||||
.foregroundColor(DamusColors.white)
|
||||
.font(.system(size: 18, weight: .ultraLight))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func show_profile_after_delay() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
|
||||
if let scanResult {
|
||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cameraAnimate(completion: @escaping () -> Void) {
|
||||
outerTrimEnd = 0.0
|
||||
withAnimation(.easeInOut(duration: animationDuration)) {
|
||||
outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done.
|
||||
}
|
||||
completion()
|
||||
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}))
|
||||
}
|
||||
|
||||
func generateQRCode(pubkey: String) -> UIImage {
|
||||
@@ -300,4 +130,3 @@ struct QRCodeView_Previews: PreviewProvider {
|
||||
QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import SwiftUI
|
||||
struct RelayFilterView: View {
|
||||
let state: DamusState
|
||||
let timeline: Timeline
|
||||
//@State var relays: [RelayDescriptor]
|
||||
//@EnvironmentObject var user_settings: UserSettingsStore
|
||||
//@State var relays: [RelayDescriptor]
|
||||
|
||||
init(state: DamusState, timeline: Timeline) {
|
||||
self.state = state
|
||||
|
||||
@@ -42,7 +42,9 @@ struct RecommendedRelayView: View {
|
||||
Text(relay).layoutPriority(1)
|
||||
|
||||
if let meta = damus.relay_metadata.lookup(relay_id: relay) {
|
||||
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta)){
|
||||
NavigationLink ( destination:
|
||||
RelayDetailView(state: damus, relay: relay, nip11: meta)
|
||||
){
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0.0)
|
||||
|
||||
@@ -72,18 +72,14 @@ struct RelayDetailView: View {
|
||||
|
||||
if let pubkey = nip11.pubkey {
|
||||
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey), label: {
|
||||
UserViewRow(damus_state: state, pubkey: pubkey)
|
||||
})
|
||||
UserViewRow(damus_state: state, pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
if let relay_connection {
|
||||
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
|
||||
HStack {
|
||||
Text(relay)
|
||||
Spacer()
|
||||
RelayStatusView(connection: relay_connection)
|
||||
}
|
||||
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
|
||||
HStack {
|
||||
Text(relay)
|
||||
Spacer()
|
||||
RelayStatus(pool: state.pool, relay: relay)
|
||||
}
|
||||
}
|
||||
if nip11.is_paid {
|
||||
@@ -92,7 +88,7 @@ struct RelayDetailView: View {
|
||||
}, header: {
|
||||
Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.")
|
||||
}, footer: {
|
||||
Text("This is a paid relay, you must pay for notes to be accepted.", comment: "Footer description that explains that the relay server requires payment to post.")
|
||||
Text("This is a paid relay, you must pay for posts to be accepted.", comment: "Footer description that explains that the relay server requires payment to post.")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,10 +134,6 @@ struct RelayDetailView: View {
|
||||
}
|
||||
return attrString
|
||||
}
|
||||
|
||||
private var relay_connection: RelayConnection? {
|
||||
state.pool.get_relay(relay)?.connection
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayDetailView_Previews: PreviewProvider {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// RelayStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayStatus: View {
|
||||
let pool: RelayPool
|
||||
let relay: String
|
||||
|
||||
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
|
||||
|
||||
@State var conn_color: Color = .gray
|
||||
@State var conn_image: String = "network"
|
||||
@State var connecting: Bool = false
|
||||
|
||||
func update_connection() {
|
||||
for relay in pool.relays {
|
||||
if relay.id == self.relay {
|
||||
let c = relay.connection
|
||||
if c.isConnected {
|
||||
conn_image = "globe"
|
||||
conn_color = .green
|
||||
} else if c.isConnecting {
|
||||
connecting = true
|
||||
} else {
|
||||
conn_image = "warning.fill"
|
||||
conn_color = .red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if connecting {
|
||||
ProgressView()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing, 5)
|
||||
} else {
|
||||
Image(conn_image)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(conn_color)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
update_connection()
|
||||
}
|
||||
.onAppear() {
|
||||
update_connection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayStatus_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayStatus(pool: test_damus_state().pool, relay: "relay")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user