Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu 2bfd60a482 Replace deprecated NIP-08 indexed mentions with NIP-27 nostr: prefixed URI text note references on writes
Changelog-Fixed: Replace deprecated NIP-08 indexed mentions with NIP-27 nostr: prefixed URI text note references on writes
2023-06-07 00:25:05 -04:00
137 changed files with 1566 additions and 2807 deletions
-18
View File
@@ -1,21 +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 ## [1.5-2] - 2023-05-30
### Added ### Added
+1 -24
View File
@@ -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. 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 ### 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 [git-send-email]: http://git-send-email.io
### Privacy ### Privacy
+12 -48
View File
@@ -18,7 +18,6 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; }; 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
@@ -132,7 +131,6 @@
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; }; 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; }; 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
@@ -171,8 +169,6 @@
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; }; 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */; };
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; }; 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
@@ -200,7 +196,6 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; }; 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; }; 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; }; 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; }; 4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; }; 4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
@@ -244,7 +239,7 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; }; 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; }; 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* 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 */; }; 4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; }; 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; }; 4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
@@ -275,12 +270,12 @@
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; }; 4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; }; 5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; };
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; }; 501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; }; 501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; }; 501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
@@ -304,7 +299,6 @@
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
@@ -371,7 +365,6 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; }; 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -574,7 +567,6 @@
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; }; 4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; }; 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; }; 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; }; 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; }; 4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -618,8 +610,6 @@
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; };
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; }; 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
@@ -647,7 +637,6 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; }; 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; }; 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; }; 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; }; 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; }; 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
@@ -693,7 +682,7 @@
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; }; 4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; }; 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; }; 4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* 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>"; }; 4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; }; 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; }; 4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
@@ -725,12 +714,12 @@
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; }; 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; }; 50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; }; 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; };
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; }; 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; }; 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; }; 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
@@ -754,7 +743,6 @@
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; }; 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>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
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>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
@@ -878,7 +866,6 @@
4C0A3F8D280F63FF000448DE /* Models */ = { 4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C9AA1462A444422003F49FD /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */, 4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
@@ -982,7 +969,6 @@
children = ( children = (
4C54AA0929A55429003E4487 /* EventGroup.swift */, 4C54AA0929A55429003E4487 /* EventGroup.swift */,
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */, 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */,
); );
path = Notifications; path = Notifications;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1157,7 +1143,6 @@
50B5685229F97CB400A23243 /* CredentialHandler.swift */, 50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */, 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */, 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
D2277EE92A089BD5006C3807 /* Router.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1171,14 +1156,6 @@
path = Buttons; path = Buttons;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4C9AA1462A444422003F49FD /* Zaps */ = {
isa = PBXGroup;
children = (
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = { 4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1188,7 +1165,7 @@
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */, 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */, F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */, 4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */, 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */, 4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */, 4CDA128929E9D10C0006FA5A /* SignalView.swift */,
); );
@@ -1219,7 +1196,6 @@
4CB9D4A52992D01900A9A7E4 /* Profile */ = { 4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CB8FC222A41ABA500763C51 /* AboutView.swift */,
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */, F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
@@ -1231,7 +1207,6 @@
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */, 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */, 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */, 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */,
); );
path = Profile; path = Profile;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1310,9 +1285,7 @@
4CE6DEE427F7A08100C66700 /* Products */, 4CE6DEE427F7A08100C66700 /* Products */,
4CEE2AE62804F57B00AB5EEF /* Frameworks */, 4CEE2AE62804F57B00AB5EEF /* Frameworks */,
); );
indentWidth = 4;
sourceTree = "<group>"; sourceTree = "<group>";
tabWidth = 4;
}; };
4CE6DEE427F7A08100C66700 /* Products */ = { 4CE6DEE427F7A08100C66700 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
@@ -1416,7 +1389,6 @@
4CE879572996C45300F758CC /* ZapsView.swift */, 4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */,
); );
path = Zaps; path = Zaps;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1786,7 +1758,6 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */, 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */, 4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
@@ -1865,8 +1836,6 @@
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */, 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
@@ -1914,7 +1883,6 @@
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */, E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */, 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
@@ -1929,12 +1897,11 @@
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */, 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */, 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */, 4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */, 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */,
@@ -1946,7 +1913,6 @@
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */, 4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
@@ -2242,7 +2208,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2260,7 +2226,6 @@
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard; INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -2291,7 +2256,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2309,7 +2274,6 @@
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard; INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -2340,7 +2304,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2360,7 +2324,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
+3 -5
View File
@@ -24,11 +24,9 @@ struct GradientButtonStyle: ButtonStyle {
struct GradientButtonStyle_Previews: PreviewProvider { struct GradientButtonStyle_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VStack { VStack {
Button(action: { Button("Dynamic Size", action: {
print("dynamic size") print("dynamic size")
}) { })
Text(verbatim: "Dynamic Size")
}
.buttonStyle(GradientButtonStyle()) .buttonStyle(GradientButtonStyle())
@@ -36,7 +34,7 @@ struct GradientButtonStyle_Previews: PreviewProvider {
print("infinite width") print("infinite width")
}) { }) {
HStack { HStack {
Text(verbatim: "Infinite Width") Text("Infinite Width")
} }
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
} }
-1
View File
@@ -53,7 +53,6 @@ enum ImageShape {
} }
// MARK: - Image Carousel // MARK: - Image Carousel
@MainActor
struct ImageCarousel: View { struct ImageCarousel: View {
var urls: [MediaUrl] var urls: [MediaUrl]
+4 -5
View File
@@ -37,7 +37,7 @@ struct InvoiceView: View {
var PayButton: some View { var PayButton: some View {
Button { Button {
if settings.show_wallet_selector { if settings.show_wallet_selector {
present_sheet(.select_wallet(invoice: invoice.string)) showing_select_wallet = true
} else { } else {
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
} }
@@ -79,6 +79,9 @@ struct InvoiceView: View {
} }
.padding(30) .padding(30)
} }
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
} }
} }
@@ -113,7 +116,3 @@ struct InvoiceView_Previews: PreviewProvider {
} }
} }
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet, sheet)
}
+1 -1
View File
@@ -18,7 +18,7 @@ struct Reposted: View {
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false) ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).") Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
} }
} }
+18 -15
View File
@@ -11,27 +11,30 @@ struct UserViewRow: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String let pubkey: String
@State var navigating: Bool = false
var body: some View { var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey) UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle()) .contentShape(Rectangle())
.background(.clear) .background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
} }
} }
struct UserView: View { struct UserView: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String let pubkey: String
let spacer: Bool
@State var about_text: Text? = nil
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
self.damus_state = damus_state
self.pubkey = pubkey
self.spacer = spacer
}
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
@@ -39,16 +42,16 @@ struct UserView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false) ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
if let about_text { if let about = profile?.about {
about_text let blocks = parse_mentions(content: about, tags: [])
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
Text(about_string)
.lineLimit(3) .lineLimit(3)
.font(.footnote) .font(.footnote)
} }
} }
if spacer { Spacer()
Spacer()
}
} }
} }
} }
+55 -25
View File
@@ -32,9 +32,10 @@ struct ZapButton: View {
let lnurl: String let lnurl: String
@ObservedObject var zaps: ZapsDataModel @ObservedObject var zaps: ZapsDataModel
@StateObject var button: ZapButtonModel = ZapButtonModel()
var our_zap: Zapping? { var our_zap: Zapping? {
zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey }) zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
} }
var zap_img: String { var zap_img: String {
@@ -55,6 +56,13 @@ struct ZapButton: View {
// always orange ! // always orange !
return Color.orange return Color.orange
/*
if our_zap.is_paid {
return Color.orange
} else {
return Color.yellow
}
*/
} }
func tap() { func tap() {
@@ -106,17 +114,15 @@ struct ZapButton: View {
var body: some View { var body: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
if !damus_state.settings.nozaps || zaps.zap_total > 0 { Button(action: {
Button(action: { }, label: {
}, label: { Image(zap_img)
Image(zap_img) .resizable()
.resizable() .foregroundColor(zap_color)
.foregroundColor(zap_color) .font(.footnote.weight(.medium))
.font(.footnote.weight(.medium)) .aspectRatio(contentMode: .fit)
.aspectRatio(contentMode: .fit) .frame(width:20, height: 20)
.frame(width:20, height: 20) })
})
}
if zaps.zap_total > 0 { if zaps.zap_total > 0 {
Text(verbatim: format_msats_abbrev(zaps.zap_total)) Text(verbatim: format_msats_abbrev(zaps.zap_total))
@@ -126,15 +132,43 @@ struct ZapButton: View {
} }
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
.simultaneousGesture(LongPressGesture().onEnded {_ in .simultaneousGesture(LongPressGesture().onEnded {_ in
guard !damus_state.settings.nozaps else { return } button.showing_zap_customizer = true
present_sheet(.zap(target: target, lnurl: lnurl))
}) })
.highPriorityGesture(TapGesture().onEnded { .highPriorityGesture(TapGesture().onEnded {
guard !damus_state.settings.nozaps else { return }
tap() tap()
}) })
.sheet(isPresented: $button.showing_zap_customizer) {
CustomizeZapView(state: damus_state, target: target, 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.target.id == self.target.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)
}
case .sent_from_nwc:
break
}
}
} }
} }
@@ -228,21 +262,17 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
} }
var flusher: OnFlush? = nil var flusher: OnFlush? = nil
// Don't donate on custom zaps
// donations are only enabled on one-tap zaps and off appstore if !is_custom && damus_state.settings.donation_percent > 0 {
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
flusher = .once({ pe in flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task { @MainActor in Task.init { @MainActor in
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
} }
}) })
} }
// we don't have a delay on one-tap nozaps (since this will be from customize zap view) let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: flusher)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
+97 -145
View File
@@ -14,38 +14,17 @@ struct TimestampedProfile {
let event: NostrEvent let event: NostrEvent
} }
struct ZapSheet {
let target: ZapTarget
let lnurl: String
}
struct SelectWallet {
let invoice: String
}
enum Sheets: Identifiable { enum Sheets: Identifiable {
case post(PostAction) case post(PostAction)
case report(ReportTarget) case report(ReportTarget)
case event(NostrEvent) case event(NostrEvent)
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter case filter
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
}
static func select_wallet(invoice: String) -> Sheets {
return .select_wallet(SelectWallet(invoice: invoice))
}
var id: String { var id: String {
switch self { switch self {
case .report: return "report" case .report: return "report"
case .post(let action): return "post-" + (action.ev?.id ?? "") case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id case .event(let ev): return "event-" + ev.id
case .zap(let sheet): return "zap-" + sheet.target.id
case .select_wallet: return "select-wallet"
case .filter: return "filter" case .filter: return "filter"
} }
} }
@@ -82,14 +61,21 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil @State var damus_state: DamusState? = nil
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home @SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var is_deleted_account: Bool = false @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 muting: String? = nil
@State var confirm_mute: Bool = false @State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false @State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false @State var confirm_overwrite_mutelist: Bool = false
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies @SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
var home: HomeModel = HomeModel() @StateObject var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
let sub_id = UUID().description let sub_id = UUID().description
@@ -121,7 +107,7 @@ struct ContentView: View {
if privkey != nil { if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) { PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post(.posting(.none)) self.active_sheet = .post(.posting)
} }
} }
} }
@@ -129,8 +115,8 @@ struct ContentView: View {
.safeAreaInset(edge: .top, spacing: 0) { .safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) { VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: { CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts) Text("Posts", comment: "Label for filter for seeing only posts (instead of posts 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 & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
}) })
Divider() Divider()
.frame(height: 1) .frame(height: 1)
@@ -142,13 +128,16 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack { ZStack {
if let damus = self.damus_state { if let damus = self.damus_state {
TimelineView(events: home.events, loading: .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() { func popToRoot() {
navigationCoordinator.popToRoot() profile_open = false
thread_open = false
search_open = false
wallet_open = false
isSideBarOpened = false isSideBarOpened = false
} }
@@ -159,6 +148,21 @@ struct ContentView: View {
func MainContent(damus: DamusState) -> some View { func MainContent(damus: DamusState) -> some View {
VStack { 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 { switch selected_timeline {
case .search: case .search:
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
@@ -200,6 +204,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 { func MaybeReportView(target: ReportTarget) -> some View {
Group { Group {
if let damus_state { if let damus_state {
@@ -215,30 +241,32 @@ struct ContentView: View {
} }
func open_event(ev: NostrEvent) { func open_event(ev: NostrEvent) {
let thread = ThreadModel(event: ev, damus_state: damus_state!) popToRoot()
navigationCoordinator.push(route: Route.Thread(thread: thread)) self.active_event = ev
self.thread_open = true
} }
func open_wallet(nwc: WalletConnectURL) { func open_wallet(nwc: WalletConnectURL) {
self.damus_state!.wallet.new(nwc) self.damus_state!.wallet.new(nwc)
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet)) self.wallet_open = true
} }
func open_profile(id: String) { func open_profile(id: String) {
let profile_model = ProfileModel(pubkey: id, damus: damus_state!) popToRoot()
let followers = FollowersModel(damus_state: damus_state!, target: id) self.active_profile = id
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers)) self.profile_open = true
} }
func open_search(filt: NostrFilter) { func open_search(filt: NostrFilter) {
let search = SearchModel(state: damus_state!, search: filt) popToRoot()
navigationCoordinator.push(route: Route.Search(search: search)) self.active_search = filt
self.search_open = true
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state { if let damus = self.damus_state {
NavigationStack(path: $navigationCoordinator.path) { NavigationView {
TabView { // Prevents navbar appearance change on scroll TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus) MainContent(damus: damus)
.toolbar() { .toolbar() {
@@ -261,14 +289,13 @@ struct ContentView: View {
if selected_timeline == .search { if selected_timeline == .search {
Button(action: { Button(action: {
//isFilterVisible.toggle() //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 // checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter") Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
.foregroundColor(.gray) .foregroundColor(.gray)
//.contentShape(Rectangle()) //.contentShape(Rectangle())
} }
.buttonStyle(.plain)
} }
} }
} }
@@ -278,16 +305,10 @@ struct ContentView: View {
.overlay( .overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation()) 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) .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) .padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea()) .background(Color(uiColor: .systemBackground).ignoresSafeArea())
} }
@@ -306,10 +327,6 @@ struct ContentView: View {
PostView(action: action, damus_state: damus_state!) PostView(action: action, damus_state: damus_state!)
case .event: case .event:
EventDetailView() EventDetailView()
case .zap(let zapsheet):
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
case .select_wallet(let select):
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
case .filter: case .filter:
let timeline = selected_timeline let timeline = selected_timeline
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
@@ -417,31 +434,6 @@ struct ContentView: View {
.onReceive(handle_notify(.unmute_thread)) { notif in .onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_events() home.filter_events()
} }
.onReceive(handle_notify(.present_sheet)) { notif in
let sheet = notif.object as! Sheets
self.active_sheet = sheet
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state!.settings.show_wallet_selector {
present_sheet(.select_wallet(invoice: inv))
} else {
let wallet = damus_state!.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
.onChange(of: scenePhase) { (phase: ScenePhase) in .onChange(of: scenePhase) { (phase: ScenePhase) in
switch phase { switch phase {
case .background: case .background:
@@ -477,8 +469,8 @@ struct ContentView: View {
switch local.type { switch local.type {
case .dm: case .dm:
selected_timeline = .dms selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey) damus_state.dms.open_dm_by_pk(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
case .like: fallthrough case .like: fallthrough
case .zap: fallthrough case .zap: fallthrough
case .mention: fallthrough case .mention: fallthrough
@@ -645,8 +637,7 @@ struct ContentView: View {
bootstrap_relays: bootstrap_relays, bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey), replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(keypair: keypair), muted_threads: MutedThreadsManager(keypair: keypair),
wallet: WalletModel(settings: settings), wallet: WalletModel(settings: settings)
nav: self.navigationCoordinator
) )
home.damus_state = self.damus_state! home.damus_state = self.damus_state!
@@ -755,57 +746,24 @@ func setup_notifications() {
} }
} }
struct FindEvent { func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
let type: FindEventType if let ev = state.events.lookup(evid) {
let find_from: [String]? callback(ev)
return
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .profile(pubkey), find_from: find_from)
}
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .event(evid), find_from: find_from)
}
}
enum FindEventType {
case profile(String)
case event(String)
}
enum FoundEvent {
case profile(Profile, NostrEvent)
case invalid_profile(NostrEvent)
case event(NostrEvent)
}
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
var filter: NostrFilter? = nil
let find_from = query_.find_from
let query = query_.type
switch query {
case .profile(let pubkey):
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
callback(.profile(profile.profile, profile.event))
return
}
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
case .event(let evid):
if let ev = state.events.lookup(evid) {
callback(.event(ev))
return
}
filter = NostrFilter(ids: [evid], limit: 1)
} }
let subid = UUID().description let subid = UUID().description
var attempts: Int = 0
var has_event = false var has_event = false
guard let filter else { return }
var filter = search_type == .event ? NostrFilter(ids: [evid]) : NostrFilter(authors: [evid])
if search_type == .profile {
filter.kinds = [.metadata]
}
filter.limit = 1
var attempts = 0
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else { guard case .nostr_event(let ev) = res else {
@@ -821,22 +779,15 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
break break
case .event(_, let ev): case .event(_, let ev):
has_event = true has_event = true
state.pool.unsubscribe(sub_id: subid) state.pool.unsubscribe(sub_id: subid)
switch query { if search_type == .profile && ev.known_kind == .metadata {
case .profile: process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) {
if ev.known_kind == .metadata { callback(ev)
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
}
} }
case .event: } else {
callback(.event(ev)) callback(ev)
} }
case .eose: case .eose:
if !has_event { if !has_event {
@@ -859,11 +810,11 @@ func timeline_name(_ timeline: Timeline?) -> String {
} }
switch timeline { switch timeline {
case .home: case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where 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: case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.") return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
case .search: case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where 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: case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.") return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
} }
@@ -961,9 +912,10 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
if ref.key == "p" { if ref.key == "p" {
result(.profile(ref.ref_id)) result(.profile(ref.ref_id))
} else if ref.key == "e" { } else if ref.key == "e" {
find_event(state: state, query: .event(evid: ref.ref_id)) { res in find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
guard let res, case .event(let ev) = res else { return } if let ev {
result(.event(ev)) result(.event(ev))
}
} }
} }
case .filter(let filt): case .filter(let filt):
+1 -1
View File
@@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject {
@Published var our_zap: Zapping? @Published var our_zap: Zapping?
@Published var likes: Int @Published var likes: Int
@Published var boosts: Int @Published var boosts: Int
@Published private(set) var zaps: Int @Published var zaps: Int
@Published var zap_total: Int64 @Published var zap_total: Int64
@Published var replies: Int @Published var replies: Int
-20
View File
@@ -11,8 +11,6 @@ import Foundation
class Contacts { class Contacts {
private var friends: Set<String> = Set() private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set() private var friend_of_friends: Set<String> = Set()
/// Tracks which friends are friends of a given pubkey.
private var pubkey_to_our_friends = [String : Set<String>]()
private var muted: Set<String> = Set() private var muted: Set<String> = Set()
let our_pubkey: String let our_pubkey: String
@@ -60,10 +58,6 @@ class Contacts {
func remove_friend(_ pubkey: String) { func remove_friend(_ pubkey: String) {
friends.remove(pubkey) friends.remove(pubkey)
pubkey_to_our_friends.forEach {
pubkey_to_our_friends[$0.key]?.remove(pubkey)
}
} }
func get_friend_list() -> [String] { func get_friend_list() -> [String] {
@@ -79,15 +73,6 @@ class Contacts {
for tag in contact.tags { for tag in contact.tags {
if tag.count >= 2 && tag[0] == "p" { if tag.count >= 2 && tag[0] == "p" {
friend_of_friends.insert(tag[1]) friend_of_friends.insert(tag[1])
// Exclude themself and us.
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
if pubkey_to_our_friends[tag[1]] == nil {
pubkey_to_our_friends[tag[1]] = Set<String>()
}
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
}
} }
} }
} }
@@ -111,11 +96,6 @@ class Contacts {
func follow_state(_ pubkey: String) -> FollowState { func follow_state(_ pubkey: String) -> FollowState {
return is_friend(pubkey) ? .follows : .unfollows return is_friend(pubkey) ? .follows : .unfollows
} }
/// Gets the list of pubkeys of our friends who follow the given pubkey.
func get_friended_followers(_ pubkey: String) -> [String] {
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
}
} }
func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? { func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {
+2 -12
View File
@@ -30,23 +30,13 @@ struct DamusState {
let replies: ReplyCounter let replies: ReplyCounter
let muted_threads: MutedThreadsManager let muted_threads: MutedThreadsManager
let wallet: WalletModel let wallet: WalletModel
let nav: NavigationCoordinator
@discardableResult @discardableResult
func add_zap(zap: Zapping) -> Bool { func add_zap(zap: Zapping) -> Bool {
// store generic zap mapping // store generic zap mapping
self.zaps.add_zap(zap: zap) self.zaps.add_zap(zap: zap)
let stored = self.events.store_zap(zap: zap)
// thread zaps
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
// [nozaps]: thread zaps are only available outside of the app store
replies.count_replies(ev)
events.add_replies(ev: ev)
}
// associate with events as well // associate with events as well
return stored return self.events.store_zap(zap: zap)
} }
var pubkey: String { var pubkey: String {
@@ -58,5 +48,5 @@ struct DamusState {
} }
static var empty: 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())) }
} }
+10
View File
@@ -30,6 +30,16 @@ class DirectMessagesModel: ObservableObject {
self.active_model = model 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) { func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey { for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model) self.set_active_dm_model(model)
+76 -119
View File
@@ -23,7 +23,7 @@ struct NewEventsBits: OptionSet {
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions] static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
} }
class HomeModel { class HomeModel: ObservableObject {
// Don't trigger a user notification for events older than a certain age // Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60 static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
@@ -49,10 +49,9 @@ class HomeModel {
var signal = SignalModel() var signal = SignalModel()
var notifications = NotificationsModel() @Published var new_events: NewEventsBits = NewEventsBits()
var notification_status = NotificationStatusModel() @Published var notifications = NotificationsModel()
var events: EventHolder = EventHolder() @Published var events: EventHolder = EventHolder()
var zap_button: ZapButtonModel = ZapButtonModel()
init() { init() {
self.damus_state = DamusState.empty self.damus_state = DamusState.empty
@@ -165,38 +164,70 @@ class HomeModel {
} }
} }
func handle_zap_event(_ ev: NostrEvent) { func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
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
}
if !self.notifications.insert_zap(.zap(zap)) { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return 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 { if handle_last_event(ev: ev, timeline: .notifications) {
return if damus_state.settings.zap_vibration {
}
if self.damus_state.settings.zap_vibration {
// Generate zap vibration // Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount) zap_vibrate(zap_amount: zap.invoice.amount)
} }
if damus_state.settings.zap_notification {
if self.damus_state.settings.zap_notification {
// Create in-app local notification for zap received. // Create in-app local notification for zap received.
switch zap.target { switch zap.target {
case .profile(let profile_id): case .profile(let profile_id):
create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id) create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id)
case .note(let note_target): 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_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_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 +402,12 @@ class HomeModel {
/// Send the initial filters, just our contact list mostly /// Send the initial filters, just our contact list mostly
func send_initial_filters(relay_id: String) { func send_initial_filters(relay_id: String) {
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey]) var filter = NostrFilter(kinds: [.contacts],
let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid) limit: 1,
pool.send(.subscribe(subscription), to: [relay_id]) 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?) { func send_home_filters(relay_id: String?) {
// TODO: since times should be based on events from a specific relay // TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow // perhaps we could mark this in the relay pool somehow
@@ -386,7 +417,7 @@ class HomeModel {
var contacts_filter = NostrFilter(kinds: [.metadata]) var contacts_filter = NostrFilter(kinds: [.metadata])
contacts_filter.authors = friends contacts_filter.authors = friends
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata]) var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
our_contacts_filter.authors = [damus_state.pubkey] our_contacts_filter.authors = [damus_state.pubkey]
@@ -441,7 +472,7 @@ class HomeModel {
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters) notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters) dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id { if let relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
@@ -524,8 +555,8 @@ class HomeModel {
@discardableResult @discardableResult
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool { func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) { if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
self.notification_status.new_events = new_bits new_events = new_bits
return true return true
} else { } else {
return false return false
@@ -557,7 +588,7 @@ class HomeModel {
} }
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) { 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 { if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message") let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
@@ -575,7 +606,7 @@ class HomeModel {
if !should_debounce_dms { if !should_debounce_dms {
self.incoming_dms.append(ev) self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: 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) got_new_dm(notifs: notifs, ev: ev)
} }
self.incoming_dms = [] self.incoming_dms = []
@@ -585,7 +616,7 @@ class HomeModel {
incoming_dms.append(ev) incoming_dms.append(ev)
dm_debouncer.debounce { [self] in dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: 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) got_new_dm(notifs: notifs, ev: ev)
} }
self.incoming_dms = [] self.incoming_dms = []
@@ -752,7 +783,7 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
switch validated { switch validated {
case .unknown: case .unknown:
Task.detached(priority: .medium) { Task {
let result = validate_event(ev: ev) let result = validate_event(ev: ev)
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -773,11 +804,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) { guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
completion?(nil) completion?()
return return
} }
@@ -785,7 +816,7 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
DispatchQueue.main.async { DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev) process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
completion?(profile) completion?()
} }
} }
} }
@@ -858,7 +889,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
if changed { if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new)) save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
state.pool.connect()
notify(.relays_changed, ()) notify(.relays_changed, ())
} }
} }
@@ -1076,8 +1106,9 @@ func zap_notification_title(_ zap: Zap) -> String {
} }
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String { func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.request.ev let src = zap.private_request ?? zap.request.ev
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey let anon = event_is_anonymous(ev: src)
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount) let formattedSats = format_msats_abbrev(zap.invoice.amount)
@@ -1165,15 +1196,13 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
create_local_notification(profiles: damus_state.profiles, notify: notify ) 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) { } 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: inner_ev.content)
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content)
create_local_notification(profiles: damus_state.profiles, notify: notify) create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification, } else if type == .like && damus_state.settings.like_notification,
let evid = ev.referenced_ids.last?.ref_id, let evid = ev.referenced_ids.last?.ref_id,
let liked_event = damus_state.events.lookup(evid) 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: liked_event.content)
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content)
create_local_notification(profiles: damus_state.profiles, notify: notify) create_local_notification(profiles: damus_state.profiles, notify: notify)
} }
@@ -1221,75 +1250,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
}
@@ -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()
}
+5 -5
View File
@@ -21,12 +21,12 @@ class ZapGroup {
} }
func zap_requests() -> [NostrEvent] { func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request.ev } zaps.map { z in z.request }
} }
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for zap in zaps { for zap in zaps {
if !isIncluded(zap.request.ev) { if !isIncluded(zap.request) {
return true return true
} }
} }
@@ -35,7 +35,7 @@ class ZapGroup {
} }
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? { func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request.ev) } let new_zaps = zaps.filter { isIncluded($0.request) }
guard new_zaps.count > 0 else { guard new_zaps.count > 0 else {
return nil return nil
} }
@@ -60,8 +60,8 @@ class ZapGroup {
msat_total += zap.amount msat_total += zap.amount
if !zappers.contains(zap.request.ev.pubkey) { if !zappers.contains(zap.request.pubkey) {
zappers.insert(zap.request.ev.pubkey) zappers.insert(zap.request.pubkey)
} }
return true return true
+3 -3
View File
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
} }
for zap in incoming_zaps { for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey) pks.insert(zap.request.pubkey)
} }
return Array(pks) return Array(pks)
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
changed = changed || incoming_events.count != count changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) } profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
changed = changed || profile_zaps.zaps.count != count changed = changed || profile_zaps.zaps.count != count
for el in reactions { for el in reactions {
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps { for el in zaps {
count = el.value.zaps.count count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter { el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev) isIncluded($0.request)
} }
changed = changed || el.value.zaps.count != count changed = changed || el.value.zaps.count != count
} }
+1 -1
View File
@@ -76,7 +76,7 @@ class ProfileModel: ObservableObject, Equatable {
profile_filter.authors = [pubkey] profile_filter.authors = [pubkey]
text_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("subscribing to profile \(pubkey) with sub_id \(sub_id)")
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
-17
View File
@@ -10,21 +10,15 @@ import Foundation
/// manages the lifetime of a thread /// manages the lifetime of a thread
class ThreadModel: ObservableObject { class ThreadModel: ObservableObject {
@Published var event: NostrEvent @Published var event: NostrEvent
let original_event: NostrEvent
var event_map: Set<NostrEvent> var event_map: Set<NostrEvent>
init(event: NostrEvent, damus_state: DamusState) { init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
self.event_map = Set() self.event_map = Set()
self.event = event self.event = event
self.original_event = event
add_event(event) add_event(event)
} }
var is_original: Bool {
return original_event.id == event.id
}
let damus_state: DamusState let damus_state: DamusState
let profiles_subid = UUID().description let profiles_subid = UUID().description
@@ -107,10 +101,6 @@ class ThreadModel: ObservableObject {
if ev.known_kind == .metadata { if ev.known_kind == .metadata {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.known_kind == .zap {
process_zap_event(damus_state: damus_state, ev: ev) { zap in
}
} else if ev.is_textlike { } else if ev.is_textlike {
self.add_event(ev) self.add_event(ev)
} }
@@ -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
})
}
+1 -5
View File
@@ -84,7 +84,7 @@ class UserSettingsStore: ObservableObject {
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild) @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
var default_media_uploader: MediaUploader var default_media_uploader: MediaUploader
@Setting(key: "show_wallet_selector", default_value: false) @Setting(key: "show_wallet_selector", default_value: true)
var show_wallet_selector: Bool var show_wallet_selector: Bool
@Setting(key: "left_handed", default_value: false) @Setting(key: "left_handed", default_value: false)
@@ -126,10 +126,6 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "truncate_timeline_text", default_value: false) @Setting(key: "truncate_timeline_text", default_value: false)
var truncate_timeline_text: Bool var truncate_timeline_text: Bool
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
@Setting(key: "nozaps", default_value: true)
var nozaps: Bool
@Setting(key: "truncate_mention_text", default_value: true) @Setting(key: "truncate_mention_text", default_value: true)
var truncate_mention_text: Bool var truncate_mention_text: Bool
+2
View File
@@ -10,4 +10,6 @@ import Foundation
class ZapButtonModel: ObservableObject { class ZapButtonModel: ObservableObject {
var invoice: String? = nil var invoice: String? = nil
@Published var zapping: String = "" @Published var zapping: String = ""
@Published var showing_select_wallet: Bool = false
@Published var showing_zap_customizer: Bool = false
} }
-29
View File
@@ -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
}
}
+6 -1
View File
@@ -53,13 +53,18 @@ class ZapsModel: ObservableObject {
case .notice: case .notice:
break break
case .eose: 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) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev): case .event(_, let ev):
guard ev.kind == 9735 else { guard ev.kind == 9735 else {
return return
} }
if let zap = state.zaps.zaps[ev.id] {
state.events.store_zap(zap: zap)
return
}
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else { guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return return
} }
+2 -2
View File
@@ -100,8 +100,8 @@ class Profile: Codable {
} }
var damus_donation: Int? { var damus_donation: Int? {
get { return int("damus_donation_v2"); } get { return int("damus_donation"); }
set(s) { set_int("damus_donation_v2", s) } set(s) { set_int("damus_donation", s) }
} }
var picture: String? { var picture: String? {
-12
View File
@@ -22,18 +22,6 @@ func encode_event_id_uri(_ ref: ReferencedId) -> String {
return "e:" + ref.ref_id return "e:" + ref.ref_id
} }
func parse_nostr_ref_uri_type(_ p: Parser) -> String? {
if parse_char(p, "p") {
return "p"
}
if parse_char(p, "e") {
return "e"
}
return nil
}
func parse_hexstr(_ p: Parser, len: Int) -> String? { func parse_hexstr(_ p: Parser, len: Int) -> String? {
var i: Int = 0 var i: Int = 0
+3 -8
View File
@@ -37,9 +37,9 @@ public struct RelayURL: Hashable {
} }
} }
final class RelayConnection: ObservableObject { final class RelayConnection {
@Published private(set) var isConnected = false private(set) var isConnected = false
@Published private(set) var isConnecting = false private(set) var isConnecting = false
private(set) var last_connection_attempt: TimeInterval = 0 private(set) var last_connection_attempt: TimeInterval = 0
private(set) var last_pong: Date? = nil private(set) var last_pong: Date? = nil
@@ -129,11 +129,6 @@ final class RelayConnection: ObservableObject {
} }
case .error(let error): case .error(let error):
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)") print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
let nserr = error as NSError
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
// ignore socket not connected?
return
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.isConnected = false self.isConnected = false
self.isConnecting = false self.isConnecting = false
+2 -7
View File
@@ -18,16 +18,11 @@ struct QueuedRequest {
let relay: String let relay: String
} }
struct SeenEvent: Hashable {
let relay_id: String
let evid: String
}
class RelayPool { class RelayPool {
var relays: [Relay] = [] var relays: [Relay] = []
var handlers: [RelayHandler] = [] var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = [] var request_queue: [QueuedRequest] = []
var seen: Set<SeenEvent> = Set() var seen: Set<String> = Set()
var counts: [String: UInt64] = [:] var counts: [String: UInt64] = [:]
private let network_monitor = NWPathMonitor() private let network_monitor = NWPathMonitor()
@@ -238,7 +233,7 @@ class RelayPool {
func record_seen(relay_id: String, event: NostrConnectionEvent) { func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event { if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev { if case .event(_, let nev) = ev {
let k = SeenEvent(relay_id: relay_id, evid: nev.id) let k = relay_id + nev.id
if !seen.contains(k) { if !seen.contains(k) {
seen.insert(k) seen.insert(k)
if counts[relay_id] == nil { if counts[relay_id] == nil {
-13
View File
@@ -13,19 +13,6 @@ enum WebSocketEvent {
case message(URLSessionWebSocketTask.Message) case message(URLSessionWebSocketTask.Message)
case disconnected(URLSessionWebSocketTask.CloseCode, String?) case disconnected(URLSessionWebSocketTask.CloseCode, String?)
case error(Error) case error(Error)
var description: String? {
switch self {
case .connected:
return "Connected"
case .message(_):
return "Received message"
case .disconnected(let close_code, let reason):
return "Disconnected: Close code: \(close_code), reason: \(reason ?? "unknown")"
case .error(let error):
return "Error: \(error)"
}
}
} }
final class WebSocket: NSObject, URLSessionWebSocketDelegate { final class WebSocket: NSObject, URLSessionWebSocketDelegate {
+1 -1
View File
@@ -38,7 +38,7 @@ enum DisplayName {
func parse_display_name(profile: Profile?, pubkey: String) -> 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.")) return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
} }
+6 -11
View File
@@ -62,7 +62,7 @@ class ZapsDataModel: ObservableObject {
} }
func confirm_nwc(reqid: String) { func confirm_nwc(reqid: String) {
guard let zap = zaps.first(where: { z in z.request.ev.id == reqid }), guard let zap = zaps.first(where: { z in z.request.id == reqid }),
case .pending(let pzap) = zap case .pending(let pzap) = zap
else { else {
return return
@@ -83,16 +83,16 @@ class ZapsDataModel: ObservableObject {
} }
func from(_ pubkey: String) -> [Zapping] { func from(_ pubkey: String) -> [Zapping] {
return self.zaps.filter { z in z.request.ev.pubkey == pubkey } return self.zaps.filter { z in z.request.pubkey == pubkey }
} }
@discardableResult @discardableResult
func remove(reqid: String) -> Bool { 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 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 return true
} }
} }
@@ -175,9 +175,6 @@ class EventCache {
@discardableResult @discardableResult
func store_zap(zap: Zapping) -> Bool { func store_zap(zap: Zapping) -> Bool {
let data = get_cache_data(zap.target.id).zaps_model let data = get_cache_data(zap.target.id).zaps_model
if let ev = zap.event {
insert(ev)
}
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap) return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
} }
@@ -185,7 +182,7 @@ class EventCache {
switch zap.target { switch zap.target {
case .note(let note_target): case .note(let note_target):
let zaps = get_cache_data(note_target.note_id).zaps_model let zaps = get_cache_data(note_target.note_id).zaps_model
zaps.remove(reqid: zap.request.ev.id) zaps.remove(reqid: zap.request.id)
case .profile: case .profile:
// these aren't stored anywhere yet // these aren't stored anywhere yet
break break
@@ -204,7 +201,6 @@ class EventCache {
return image_metadata[url.absoluteString.lowercased()] return image_metadata[url.absoluteString.lowercased()]
} }
@MainActor
func lookup_media_size(url: URL) -> CGSize? { func lookup_media_size(url: URL) -> CGSize? {
if let img_meta = lookup_img_metadata(url: url) { if let img_meta = lookup_img_metadata(url: url) {
return img_meta.meta.dim?.size return img_meta.meta.dim?.size
@@ -217,7 +213,6 @@ class EventCache {
video_meta[url.absoluteString] = meta video_meta[url.absoluteString] = meta
} }
@MainActor
func get_video_player_model(url: URL) -> VideoPlayerModel { func get_video_player_model(url: URL) -> VideoPlayerModel {
if let model = video_meta[url.absoluteString] { if let model = video_meta[url.absoluteString] {
return model return model
@@ -400,7 +395,7 @@ func preload_image(url: URL) {
print("Preloading image \(url.absoluteString)") 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)") print("Preloaded image \(url.absoluteString)")
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import Foundation
class EventHolder: ObservableObject, ScrollQueue { class EventHolder: ObservableObject, ScrollQueue {
private var has_event: Set<String> private var has_event: Set<String>
@Published var events: [NostrEvent] @Published var events: [NostrEvent]
var incoming: [NostrEvent] @Published var incoming: [NostrEvent]
var should_queue: Bool var should_queue: Bool
var on_queue: ((NostrEvent) -> Void)? var on_queue: ((NostrEvent) -> Void)?
+1 -1
View File
@@ -40,7 +40,7 @@ extension KFOptionSetter {
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self { func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self } guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key) let imageResource = ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource() let source = imageResource.convertToSource()
options.alternativeSources = [source] options.alternativeSources = [source]
+1 -1
View File
@@ -60,7 +60,7 @@ func hashtag_str(_ htag: String) -> CompatibleText {
} }
text = Text(attributedString) text = Text(attributedString)
let img = Image("\(name)-hashtag") let img = Image("\(name)-hashtag")
text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0) text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
} else { } else {
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = DamusColors.purple
} }
+4 -2
View File
@@ -59,15 +59,17 @@ struct ImageMetadata: Equatable {
} }
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? { 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)) let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
guard let img = UIImage.init(blurHash: blurhash, size: size) else { guard let img = UIImage.init(blurHash: blurhash, size: size) else {
let noimg: UIImage? = nil let noimg: UIImage? = nil
return noimg return noimg
} }
return img return img
} }
return await res.value return await res.value
} }
@@ -144,7 +146,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
return nil return nil
} }
let res = Task.detached(priority: .low) { let res = Task.init {
let bhs = get_blurhash_size(img_size: img.size) let bhs = get_blurhash_size(img_size: img.size)
let smaller = img.resized(to: bhs) let smaller = img.resized(to: bhs)
+2 -2
View File
@@ -11,10 +11,10 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi
var i: Int = 0 var i: Int = 0
for zap in zaps { for zap in zaps {
if new_zap.request.ev.id == zap.request.ev.id { if new_zap.request.id == zap.request.id {
// replace pending // replace pending
if !new_zap.is_pending && zap.is_pending { if !new_zap.is_pending && zap.is_pending {
print("nwc: replacing pending with real zap \(new_zap.request.ev.id)") print("nwc: replacing pending with real zap \(new_zap.request.id)")
zaps[i] = new_zap zaps[i] = new_zap
return true return true
} }
-1
View File
@@ -9,7 +9,6 @@ import Foundation
import secp256k1 import secp256k1
let PUBKEY_HRP = "npub" let PUBKEY_HRP = "npub"
let ANON_PUBKEY = "anon"
struct FullKeypair: Equatable { struct FullKeypair: Equatable {
let pubkey: String let pubkey: String
-3
View File
@@ -77,9 +77,6 @@ extension Notification.Name {
static var update_stats: Notification.Name { static var update_stats: Notification.Name {
return Notification.Name("update_stats") return Notification.Name("update_stats")
} }
static var present_sheet: Notification.Name {
return Notification.Name("present_sheet")
}
static var zapping: Notification.Name { static var zapping: Notification.Name {
return Notification.Name("zapping") return Notification.Name("zapping")
} }
-276
View File
@@ -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 = []
}
}
+9 -28
View File
@@ -41,16 +41,7 @@ public enum ZapTarget: Equatable {
struct ZapRequest { struct ZapRequest {
let ev: NostrEvent let ev: NostrEvent
let marked_hidden: Bool
var is_in_thread: Bool {
return !self.ev.content.isEmpty && !marked_hidden
}
init(ev: NostrEvent) {
self.ev = ev
self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0] == "hidden" }) != nil
}
} }
enum ExtPendingZapStateType { enum ExtPendingZapStateType {
@@ -138,7 +129,7 @@ struct ZapRequestId: Equatable {
let reqid: String let reqid: String
init(from_zap: Zapping) { init(from_zap: Zapping) {
self.reqid = from_zap.request.ev.id self.reqid = from_zap.request.id
} }
init(from_makezap: MakeZapRequest) { init(from_makezap: MakeZapRequest) {
@@ -207,12 +198,12 @@ enum Zapping {
} }
} }
var request: ZapRequest { var request: NostrEvent {
switch self { switch self {
case .zap(let zap): case .zap(let zap):
return zap.request return zap.request_ev
case .pending(let pzap): 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 { var is_anon: Bool {
switch self { switch self {
case .zap(let zap): case .zap(let zap):
@@ -260,12 +242,12 @@ struct Zap {
public let invoice: ZapInvoice public let invoice: ZapInvoice
public let zapper: String /// zap authorizer public let zapper: String /// zap authorizer
public let target: ZapTarget public let target: ZapTarget
public let raw_request: ZapRequest public let request: ZapRequest
public let is_anon: Bool public let is_anon: Bool
public let private_request: ZapRequest? public let private_request: NostrEvent?
var request: ZapRequest { var request_ev: NostrEvent {
return private_request ?? self.raw_request return private_request ?? self.request.ev
} }
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? { public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
@@ -313,9 +295,8 @@ struct Zap {
} }
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req) let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
let preq = private_request.map { pr in ZapRequest(ev: pr) }
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, 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
View File
@@ -12,8 +12,8 @@ class Zaps {
let our_pubkey: String let our_pubkey: String
var our_zaps: [String: [Zapping]] var our_zaps: [String: [Zapping]]
private(set) var event_counts: [String: Int] var event_counts: [String: Int]
private(set) var event_totals: [String: Int64] var event_totals: [String: Int64]
init(our_pubkey: String) { init(our_pubkey: String) {
self.zaps = [:] self.zaps = [:]
@@ -27,13 +27,13 @@ class Zaps {
var res: Zapping? = nil var res: Zapping? = nil
for kv in our_zaps { for kv in our_zaps {
let ours = kv.value let ours = kv.value
guard let zap = ours.first(where: { z in z.request.ev.id == reqid }) else { guard let zap = ours.first(where: { z in z.request.id == reqid }) else {
continue continue
} }
res = zap 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] { if let count = event_counts[zap.target.id] {
event_counts[zap.target.id] = count - 1 event_counts[zap.target.id] = count - 1
@@ -51,16 +51,13 @@ class Zaps {
} }
func add_zap(zap: Zapping) { func add_zap(zap: Zapping) {
if zaps[zap.request.ev.id] != nil { if zaps[zap.request.id] != nil {
return return
} }
self.zaps[zap.request.ev.id] = zap self.zaps[zap.request.id] = zap
if let zap_id = zap.event?.id {
self.zaps[zap_id] = zap
}
// record our zaps for an event // record our zaps for an event
if zap.request.ev.pubkey == our_pubkey { if zap.request.pubkey == our_pubkey {
switch zap.target { switch zap.target {
case .note(let note_target): case .note(let note_target):
if our_zaps[note_target.note_id] == nil { if our_zaps[note_target.note_id] == nil {
@@ -74,7 +71,7 @@ class Zaps {
} }
// don't count tips to self. lame. // 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 return
} }
+2 -2
View File
@@ -62,7 +62,7 @@ struct EventActionBar: View {
self.show_repost_action = true 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)" : "")") Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray) .foregroundColor(bar.boosted ? Color.green : Color.gray)
@@ -95,7 +95,7 @@ struct EventActionBar: View {
EventActionButton(img: "upload", col: Color.gray) { EventActionButton(img: "upload", col: Color.gray) {
show_share_action = true show_share_action = true
} }
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note")) .accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
} }
.onAppear { .onAppear {
self.bar.update(damus: damus_state, evid: self.event.id) self.bar.update(damus: damus_state, evid: self.event.id)
+4 -3
View File
@@ -25,7 +25,7 @@ struct EventDetailBar: View {
var body: some View { var body: some View {
HStack { HStack {
if bar.boosts > 0 { 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) 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'.") 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 { 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) 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'.") 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 { 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) 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'.") 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'.")
} }
+1
View File
@@ -38,6 +38,7 @@ struct BookmarksView: View {
} else { } else {
ScrollView { ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter) InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
} }
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@ let carousel_items = [
CarouselItem(image: Image("undercover"), CarouselItem(image: Image("undercover"),
text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")), text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")),
CarouselItem(image: Image("bitcoin-p2p"), CarouselItem(image: Image("bitcoin-p2p"),
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your 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 { struct CarouselView: View {
+18 -17
View File
@@ -18,16 +18,16 @@ struct ConfigView: View {
@State var delete_account_warning: Bool = false @State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false @State var confirm_delete_account: Bool = false
@State var delete_text: String = "" @State var delete_text: String = ""
@ObservedObject var settings: UserSettingsStore @ObservedObject var settings: UserSettingsStore
private let DELETE_KEYWORD = "DELETE" private let DELETE_KEYWORD = "DELETE"
init(state: DamusState) { init(state: DamusState) {
self.state = state self.state = state
_settings = ObservedObject(initialValue: state.settings) _settings = ObservedObject(initialValue: state.settings)
} }
func textColor() -> Color { func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
@@ -36,30 +36,31 @@ struct ConfigView: View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Form { Form {
Section { 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) 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) 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) 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) 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) 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) 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")) { Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: { Button(action: {
@@ -115,11 +116,11 @@ struct ConfigView: View {
guard let full_kp = state.keypair.to_full() else { guard let full_kp = state.keypair.to_full() else {
return return
} }
guard delete_text == DELETE_KEYWORD else { guard delete_text == DELETE_KEYWORD else {
return return
} }
let ev = created_deleted_account_profile(keypair: full_kp) let ev = created_deleted_account_profile(keypair: full_kp)
state.postbox.send(ev) state.postbox.send(ev)
notify(.logout, ()) notify(.logout, ())
@@ -163,7 +164,7 @@ func handle_string_amount(new_value: String) -> Int? {
guard let amt = NumberFormatter().number(from: filtered) as? Int else { guard let amt = NumberFormatter().number(from: filtered) as? Int else {
return nil return nil
} }
return amt return amt
} }
+11 -6
View File
@@ -10,7 +10,8 @@ import SwiftUI
struct CreateAccountView: View { struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel() @StateObject var account: CreateAccountModel = CreateAccountModel()
@StateObject var profileUploadViewModel = ProfileUploadingViewModel() @StateObject var profileUploadViewModel = ProfileUploadingViewModel()
var nav: NavigationCoordinator
@State var is_done: Bool = false
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View { func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content) return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -24,6 +25,10 @@ struct CreateAccountView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
EmptyView()
}
VStack { VStack {
VStack(alignment: .center) { VStack(alignment: .center) {
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
@@ -58,10 +63,10 @@ struct CreateAccountView: View {
.padding(.top, 10) .padding(.top, 10)
Button(action: { Button(action: {
nav.push(route: Route.SaveKeys(account: account)) self.is_done = true
}) { }) {
HStack { HStack {
Text("Create account now", comment: "Button to create account.") Text("Create account now", comment: "Button to create account.")
.fontWeight(.semibold) .fontWeight(.semibold)
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
@@ -91,7 +96,7 @@ struct LoginPrompt: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
HStack { HStack {
Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr") Text("Already on nostr?", comment: "Ask the user if they already have an account on nostr")
.foregroundColor(Color("DamusMediumGrey")) .foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) { Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
@@ -130,7 +135,7 @@ extension View {
struct CreateAccountView_Previews: PreviewProvider { struct CreateAccountView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let model = CreateAccountModel(real: "", nick: "jb55", about: "") 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) Text(title)
.bold() .bold()
if optional { if optional {
Text("optional", comment: "Label indicating that a form input is optional.") Text("- optional", comment: "Label indicating that a form input is optional.")
.font(.callout) .font(.callout)
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
} }
+2 -1
View File
@@ -61,7 +61,8 @@ struct DMChatView: View, KeyboardReadable {
var Header: some View { var Header: some View {
let profile = damus_state.profiles.lookup(id: pubkey) 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 { HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
+6 -3
View File
@@ -18,9 +18,13 @@ struct DirectMessagesView: View {
@State var dm_type: DMType = .friend @State var dm_type: DMType = .friend
@ObservedObject var model: DirectMessagesModel @ObservedObject var model: DirectMessagesModel
@ObservedObject var settings: UserSettingsStore @ObservedObject var settings: UserSettingsStore
func MainContent(requests: Bool) -> some View { func MainContent(requests: Bool) -> some View {
ScrollView { ScrollView {
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
NavigationLink(destination: chat, isActive: $model.open_dm) {
EmptyView()
}
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
if model.dms.isEmpty, !model.loading { if model.dms.isEmpty, !model.loading {
EmptyTimelineView() EmptyTimelineView()
@@ -50,8 +54,7 @@ struct DirectMessagesView: View {
if ok, let ev = model.events.last { if ok, let ev = model.events.last {
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options) EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
.onTapGesture { .onTapGesture {
self.model.set_active_dm_model(model) self.model.open_dm_by_model(model)
damus_state.nav.push(route: Route.DMChat(dms: self.model.active_model))
} }
Divider() Divider()
+10 -4
View File
@@ -56,13 +56,18 @@ By using our Application, you signify your acceptance of this EULA. If you do no
""" """
struct EULAView: View { struct EULAView: View {
@State private var login = false
@State var accepted = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var nav: NavigationCoordinator
var body: some View { var body: some View {
ZStack { ZStack {
ScrollView { ScrollView {
NavigationLink(destination: LoginView(accepted: $accepted), isActive: $login) {
EmptyView()
}
Text(Markdown.parse(content: eula)) Text(Markdown.parse(content: eula))
.padding() .padding()
} }
@@ -91,7 +96,8 @@ struct EULAView: View {
} }
Button(action: { Button(action: {
nav.push(route: Route.Login) accepted = true
login.toggle()
}) { }) {
HStack { HStack {
Text("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.") 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(), .ignoresSafeArea(),
alignment: .top 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) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav()) .navigationBarItems(leading: BackNav())
@@ -120,6 +126,6 @@ struct EULAView: View {
struct EULAView_Previews: PreviewProvider { struct EULAView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
EULAView(nav: .init()) EULAView()
} }
} }
+1 -2
View File
@@ -38,9 +38,8 @@ struct EventView: View {
} }
} else if event.known_kind == .zap { } else if event.known_kind == .zap {
if let zap = damus.zaps.zaps[event.id] { if let zap = damus.zaps.zaps[event.id] {
ZapEvent(damus: damus, zap: zap, is_top_zap: options.contains(.top_zap)) ZapEvent(damus: damus, zap: zap)
} else { } else {
Text("Invalid Zap", comment: "Text indicating that a zap event is malformed and could not be displayed.")
EmptyView() EmptyView()
} }
} else { } else {
+2 -1
View File
@@ -72,7 +72,8 @@ struct BuilderEventView: View {
if let event { if let event {
let ev = event.get_inner_event(cache: damus.events) ?? event let ev = event.get_inner_event(cache: damus.events) ?? event
let thread = ThreadModel(event: ev, damus_state: damus) 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) EventView(damus: damus, event: event, options: .embedded)
.padding([.top, .bottom], 8) .padding([.top, .bottom], 8)
}.buttonStyle(.plain) }.buttonStyle(.plain)
+1 -1
View File
@@ -37,7 +37,7 @@ struct EventProfile: View {
var body: some View { var body: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
VStack { 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) ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
} }
} }
+2 -2
View File
@@ -31,9 +31,9 @@ struct MutedEventView: View {
.foregroundColor(DamusColors.adaptableGrey) .foregroundColor(DamusColors.adaptableGrey)
HStack { 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() 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() shown.toggle()
} }
} }
+1 -2
View File
@@ -18,7 +18,6 @@ struct EventViewOptions: OptionSet {
static let no_translate = EventViewOptions(rawValue: 1 << 6) static let no_translate = EventViewOptions(rawValue: 1 << 6)
static let small_pfp = EventViewOptions(rawValue: 1 << 7) static let small_pfp = EventViewOptions(rawValue: 1 << 7)
static let nested = EventViewOptions(rawValue: 1 << 8) static let nested = EventViewOptions(rawValue: 1 << 8)
static let top_zap = EventViewOptions(rawValue: 1 << 9)
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
} }
@@ -132,7 +131,7 @@ struct TextEvent: View {
func ProfileName(is_anon: Bool) -> some View { func ProfileName(is_anon: Bool) -> some View {
let profile = damus.profiles.lookup(id: pubkey) 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) return EventProfileName(pubkey: pk, profile: profile, damus: damus, size: .normal)
} }
+7 -17
View File
@@ -10,23 +10,13 @@ import SwiftUI
struct ZapEvent: View { struct ZapEvent: View {
let damus: DamusState let damus: DamusState
let zap: Zapping let zap: Zapping
let is_top_zap: Bool
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .center) { HStack(alignment: .center) {
Image("zap.fill") Text("⚡️ \(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
.foregroundColor(.orange)
Text(verbatim: format_msats(zap.amount))
.font(.headline) .font(.headline)
.padding([.top], 2)
if is_top_zap {
Text("Top Zap", comment: "Text indicating that this zap is the one with the highest amount of sats.")
.font(.caption)
.foregroundColor(.gray)
.padding([.top], 2)
}
if zap.is_private { if zap.is_private {
Image("lock") Image("lock")
@@ -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) .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_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734) let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
let test_zap_request = ZapRequest(ev: test_zap_request_ev) let test_zap_request = ZapRequest(ev: test_zap_request_ev)
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), 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))) let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
struct ZapEvent_Previews: PreviewProvider { struct ZapEvent_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VStack { VStack {
ZapEvent(damus: test_damus_state(), zap: .zap(test_zap), 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))
} }
} }
} }
+10 -23
View File
@@ -12,13 +12,16 @@ struct FollowUserView: View {
let damus_state: DamusState let damus_state: DamusState
static let markdown = Markdown() static let markdown = Markdown()
@State var navigating: Bool = false
var body: some View { var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
HStack { HStack {
UserViewRow(damus_state: damus_state, pubkey: target.pubkey) 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)) 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 { struct FollowersView: View {
let damus_state: DamusState let damus_state: DamusState
@ObservedObject var followers: FollowersModel let whos: String
@EnvironmentObject var followers: FollowersModel
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -71,7 +58,7 @@ struct FollowingView: View {
let damus_state: DamusState let damus_state: DamusState
let following: FollowingModel let following: FollowingModel
let whos: String
var body: some View { var body: some View {
ScrollView { ScrollView {
+16 -9
View File
@@ -33,11 +33,13 @@ enum ParsedKey {
} }
struct LoginView: View { struct LoginView: View {
@State private var create_account = false
@State var key: String = "" @State var key: String = ""
@State var is_pubkey: Bool = false @State var is_pubkey: Bool = false
@State var error: String? = nil @State var error: String? = nil
@State private var credential_handler = CredentialHandler() @State private var credential_handler = CredentialHandler()
var nav: NavigationCoordinator
@Binding var accepted: Bool
func get_error(parsed_key: ParsedKey?) -> String? { func get_error(parsed_key: ParsedKey?) -> String? {
if self.error != nil { if self.error != nil {
@@ -53,6 +55,12 @@ struct LoginView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
if accepted {
NavigationLink(destination: CreateAccountView(), isActive: $create_account) {
EmptyView()
}
}
VStack { VStack {
SignInHeader() SignInHeader()
.padding(.top, 100) .padding(.top, 100)
@@ -72,10 +80,9 @@ struct LoginView: View {
} }
if parsed?.is_pub ?? false { if parsed?.is_pub ?? false {
Text("This is a public key, you will not be able to make 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) .foregroundColor(Color.orange)
.bold() .bold()
.fixedSize(horizontal: false, vertical: true)
} }
if let p = parsed { if let p = parsed {
@@ -99,7 +106,7 @@ struct LoginView: View {
.padding(.top, 10) .padding(.top, 10)
} }
CreateAccountPrompt(nav: nav) CreateAccountPrompt(create_account: $create_account)
.padding(.top, 10) .padding(.top, 10)
Spacer() Spacer()
@@ -329,14 +336,14 @@ struct SignInEntry: View {
} }
struct CreateAccountPrompt: View { struct CreateAccountPrompt: View {
var nav: NavigationCoordinator @Binding var create_account: Bool
var body: some View { var body: some View {
HStack { HStack {
Text("New to Nostr?", comment: "Ask the user if they are new to Nostr") Text("New to nostr?", comment: "Ask the user if they are new to nostr")
.foregroundColor(Color("DamusMediumGrey")) .foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) { Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
nav.push(route: Route.CreateAccount) create_account.toggle()
} }
Spacer() Spacer()
@@ -350,8 +357,8 @@ struct LoginView_Previews: PreviewProvider {
let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955" let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955"
let bech32_pubkey = "KeyInput" let bech32_pubkey = "KeyInput"
Group { Group {
LoginView(key: pubkey, nav: .init()) LoginView(key: pubkey, accepted: .constant(true))
LoginView(key: bech32_pubkey, nav: .init()) LoginView(key: bech32_pubkey, accepted: .constant(true))
} }
} }
} }
+8 -8
View File
@@ -29,7 +29,7 @@ struct TabButton: View {
let timeline: Timeline let timeline: Timeline
let img: String let img: String
@Binding var selected: Timeline @Binding var selected: Timeline
@ObservedObject var nstatus: NotificationStatusModel @Binding var new_events: NewEventsBits
let settings: UserSettingsStore let settings: UserSettingsStore
let action: (Timeline) -> () let action: (Timeline) -> ()
@@ -38,7 +38,7 @@ struct TabButton: View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
Tab Tab
if show_indicator(timeline: timeline, current: nstatus.new_events, indicator_setting: settings.notification_indicators) { if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) {
Circle() Circle()
.size(CGSize(width: 8, height: 8)) .size(CGSize(width: 8, height: 8))
.frame(width: 10, height: 10, alignment: .topTrailing) .frame(width: 10, height: 10, alignment: .topTrailing)
@@ -53,7 +53,7 @@ struct TabButton: View {
Button(action: { Button(action: {
action(timeline) action(timeline)
let bits = timeline_to_notification_bits(timeline, ev: nil) let bits = timeline_to_notification_bits(timeline, ev: nil)
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") Image(selected != timeline ? img : "\(img).fill")
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -65,7 +65,7 @@ struct TabButton: View {
struct TabBar: View { struct TabBar: View {
var nstatus: NotificationStatusModel @Binding var new_events: NewEventsBits
@Binding var selected: Timeline @Binding var selected: Timeline
let settings: UserSettingsStore let settings: UserSettingsStore
@@ -75,10 +75,10 @@ struct TabBar: View {
VStack { VStack {
Divider() Divider()
HStack { HStack {
TabButton(timeline: .home, img: "home", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("1") TabButton(timeline: .home, img: "home", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
TabButton(timeline: .dms, img: "messages", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("2") TabButton(timeline: .dms, img: "messages", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
TabButton(timeline: .search, img: "search", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("3") TabButton(timeline: .search, img: "search", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("4") TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
} }
} }
} }
-3
View File
@@ -42,9 +42,6 @@ struct MutelistView: View {
.swipeActions { .swipeActions {
RemoveAction(pubkey: pubkey) RemoveAction(pubkey: pubkey)
} }
.onTapGesture {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
}
} }
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users.")) .navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
.onAppear { .onAppear {
+1 -1
View File
@@ -425,7 +425,7 @@ enum UrlType {
case .image(let url): case .image(let url):
return url return url
case .video: case .video:
return nil return url
} }
case .link: case .link:
return nil return nil
+36 -69
View File
@@ -14,15 +14,6 @@ enum EventGroupType {
case zap(ZapGroup) case zap(ZapGroup)
case profile_zap(ZapGroup) case profile_zap(ZapGroup)
var is_note_zap: Bool {
switch self {
case .repost: return false
case .reaction: return false
case .zap: return true
case .profile_zap: return false
}
}
var zap_group: ZapGroup? { var zap_group: ZapGroup? {
switch self { switch self {
case .profile_zap(let grp): case .profile_zap(let grp):
@@ -51,7 +42,7 @@ enum EventGroupType {
} }
enum ReactingTo { enum ReactingTo {
case your_note case your_post
case tagged_in case tagged_in
case your_profile case your_profile
} }
@@ -62,7 +53,7 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
} }
if ev.pubkey == our_pubkey { if ev.pubkey == our_pubkey {
return .your_note return .your_post
} }
return .tagged_in return .tagged_in
@@ -73,42 +64,19 @@ func event_author_name(profiles: Profiles, pubkey: String) -> String {
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50) return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50)
} }
func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [String] { func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
var seen = Set<String>()
var sorted = [String]()
if let zapgrp = group.zap_group { if let zapgrp = group.zap_group {
let zaps = zapgrp.zaps let zap = zapgrp.zaps[ind]
for i in 0..<zaps.count { if zap.is_anon {
let zap = zapgrp.zaps[i] return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
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)
}
} }
return event_author_name(profiles: profiles, pubkey: zap.request.pubkey)
} else { } else {
let events = group.events let ev = group.events[ind]
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
for i in 0..<events.count {
let ev = events[i]
let pubkey = ev.pubkey
if !seen.contains(pubkey) {
seen.insert(pubkey)
sorted.append(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_1" - returned when 1 reaction occurred to a post that the current user was tagged in
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in "reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in "reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
"reacted_your_note_1" - returned when 1 reaction occurred to the current user's post "reacted_your_post_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_post_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_3" - returned when 3 or more reactions occurred to the current user's post
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile "reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile "reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile "reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
@@ -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_1" - returned when 1 repost occurred to a post that the current user was tagged in
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in "reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in "reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
"reposted_your_note_1" - returned when 1 repost occurred to the current user's post "reposted_your_post_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_post_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_3" - returned when 3 or more reposts occurred to the current user's post
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile "reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile "reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile "reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
@@ -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_1" - returned when 1 zap occurred to a post that the current user was tagged in
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in "zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in "zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
"zapped_your_note_1" - returned when 1 zap occurred to the current user's post "zapped_your_post_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_post_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_3" - returned when 3 or more zaps occurred to the current user's post
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile "zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile "zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile "zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
*/ */
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, 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 { if group.events.count == 0 {
return "??" return "??"
} }
let verb = reacting_to_verb(group: group) let verb = reacting_to_verb(group: group)
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev) let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev)
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) let format = localizedStringFormat(key: localization_key, locale: locale)
switch pubkeys.count { switch group.events.count {
case 1: 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) return String(format: format, locale: locale, display_name)
case 2: case 2:
let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0]) let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let bob_name = event_author_name(profiles: profiles, pubkey: pubkeys[1]) let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
return String(format: format, locale: locale, alice_name, bob_name) return String(format: format, locale: locale, alice_name, bob_name)
default: default:
let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0]) let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let count = pubkeys.count - 1 let count = group.events.count - 1
return String(format: format, locale: locale, count, alice_name) return String(format: format, locale: locale, count, alice_name)
} }
@@ -192,9 +160,9 @@ struct EventGroupView: View {
let state: DamusState let state: DamusState
let event: NostrEvent? let event: NostrEvent?
let group: EventGroupType let group: EventGroupType
func GroupDescription(_ pubkeys: [String]) -> some View { var GroupDescription: some View {
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, pubkeys: pubkeys))") Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))")
} }
func ZapIcon(_ zapgrp: ZapGroup) -> some View { func ZapIcon(_ zapgrp: ZapGroup) -> some View {
@@ -234,15 +202,14 @@ struct EventGroupView: View {
.frame(width: PFP_SIZE + 10) .frame(width: PFP_SIZE + 10)
VStack(alignment: .leading) { VStack(alignment: .leading) {
let unique_pubkeys = event_group_unique_pubkeys(profiles: state.profiles, group: group) ProfilePicturesView(state: state, pubkeys: group.events.map { $0.pubkey })
ProfilePicturesView(state: state, pubkeys: unique_pubkeys)
if let event { if let event {
let thread = ThreadModel(event: event, damus_state: state) 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) { VStack(alignment: .leading) {
GroupDescription(unique_pubkeys) GroupDescription
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content]) EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])
.padding([.top], 1) .padding([.top], 1)
.padding([.trailing]) .padding([.trailing])
@@ -251,7 +218,7 @@ struct EventGroupView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} else { } else {
GroupDescription(unique_pubkeys) GroupDescription
} }
} }
} }
@@ -59,7 +59,7 @@ struct NotificationItemView: View {
EventGroupView(state: state, event: ev, group: .reaction(evgrp)) EventGroupView(state: state, event: ev, group: .reaction(evgrp))
case .reply(let ev): 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) EventView(damus: state, event: ev, options: options)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -10,13 +10,20 @@ import SwiftUI
struct ProfilePicturesView: View { struct ProfilePicturesView: View {
let state: DamusState let state: DamusState
let pubkeys: [String] let pubkeys: [String]
@State var nav_target: String? = nil
@State var navigating: Bool = false
var body: some View { var body: some View {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
EmptyView()
}
HStack { HStack {
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in 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) ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
.onTapGesture { .onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) nav_target = pubkey
navigating = true
} }
} }
} }
+15 -1
View File
@@ -51,7 +51,21 @@ struct ParticipantsView: View {
ForEach(originalReferences.pRefs) { participant in ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id let pubkey = participant.id
HStack { 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") Image("check-circle.fill")
.font(.system(size: 30)) .font(.system(size: 30))
+17 -64
View File
@@ -13,21 +13,16 @@ enum NostrPostResult {
case cancel 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 { class TagModel: ObservableObject {
var diff = 0 var diff = 0
} }
enum PostTarget {
case none
case user(String)
}
enum PostAction { enum PostAction {
case replying_to(NostrEvent) case replying_to(NostrEvent)
case quoting(NostrEvent) case quoting(NostrEvent)
case posting(PostTarget) case posting
var ev: NostrEvent? { var ev: NostrEvent? {
switch self { switch self {
@@ -84,27 +79,13 @@ struct PostView: View {
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String { if let link = attributes[.link] as? String {
let normalized_link: String post.replaceCharacters(in: range, with: link)
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}")
} }
} }
var content = self.post.string 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) .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
.replacingOccurrences(of: "\u{200B}", with: "") // these characters are added when adding mentions.
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ") let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
@@ -130,14 +111,6 @@ struct PostView: View {
var is_post_empty: Bool { var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
} }
var uploading_disabled: Bool {
return image_upload.progress != nil
}
var posting_disabled: Bool {
return is_post_empty || uploading_disabled
}
var ImageButton: some View { var ImageButton: some View {
Button(action: { Button(action: {
@@ -162,7 +135,7 @@ struct PostView: View {
ImageButton ImageButton
CameraButton CameraButton
} }
.disabled(uploading_disabled) .disabled(image_upload.progress != nil)
} }
var PostButton: some View { var PostButton: some View {
@@ -173,29 +146,18 @@ struct PostView: View {
self.send_post() self.send_post()
} }
} }
.disabled(posting_disabled) .disabled(is_post_empty)
.font(.system(size: 14, weight: .bold)) .font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30) .frame(width: 80, height: 30)
.foregroundColor(.white) .foregroundColor(.white)
.background(LINEAR_GRADIENT) .background(LINEAR_GRADIENT)
.opacity(posting_disabled ? 0.5 : 1.0) .opacity(is_post_empty ? 0.5 : 1.0)
.clipShape(Capsule()) .clipShape(Capsule())
} }
func isEmpty() -> Bool { var isEmpty: Bool {
return self.uploadedMedias.count == 0 && self.uploadedMedias.count == 0 &&
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines) == self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
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)
} }
func clear_draft() { func clear_draft() {
@@ -210,17 +172,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 { guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
self.post = NSMutableAttributedString("") self.post = NSMutableAttributedString("")
self.uploadedMedias = [] self.uploadedMedias = []
return
return false
} }
self.uploadedMedias = draft.media self.uploadedMedias = draft.media
self.post = draft.content self.post = draft.content
return true
} }
func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) { func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
@@ -361,11 +321,6 @@ struct PostView: View {
.padding(.horizontal) .padding(.horizontal)
} }
func fill_target_content(target: PostTarget) {
self.post = initialString()
self.tagModel.diff = post.string.count
}
var body: some View { var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -435,7 +390,7 @@ struct PostView: View {
} }
} }
.onAppear() { .onAppear() {
let loaded_draft = load_draft() load_draft()
switch action { switch action {
case .replying_to(let replying_to): case .replying_to(let replying_to):
@@ -444,10 +399,8 @@ struct PostView: View {
case .quoting(let quoting): case .quoting(let quoting):
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting) references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
originalReferences = references originalReferences = references
case .posting(let target): case .posting:
guard !loaded_draft else { break } break
fill_target_content(target: target)
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -455,7 +408,7 @@ struct PostView: View {
} }
} }
.onDisappear { .onDisappear {
if isEmpty() { if isEmpty {
clear_draft() clear_draft()
} }
} }
@@ -495,7 +448,7 @@ func get_searching_string(_ word: String?) -> String? {
struct PostView_Previews: PreviewProvider { struct PostView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PostView(action: .posting(.none), damus_state: test_damus_state()) PostView(action: .posting, damus_state: test_damus_state())
} }
} }
+21 -16
View File
@@ -39,7 +39,7 @@ struct UserSearch: View {
guard let pk = bech32_pubkey(user.pubkey) else { guard let pk = bech32_pubkey(user.pubkey) else {
return return
} }
let tagAttributedString = user_tag_attr_string(profile: user.profile, pubkey: pk) let tagAttributedString = createUserTag(for: user, with: pk)
appendUserTag(withTag: tagAttributedString) appendUserTag(withTag: tagAttributedString)
} }
@@ -57,6 +57,26 @@ struct UserSearch: View {
newCursorIndex = wordRange.location + tagAttributedString.string.count 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.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:\(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 { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Divider() Divider()
@@ -148,18 +168,3 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search
return matches 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
}
-53
View File
@@ -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)
}
}
+2 -4
View File
@@ -103,15 +103,13 @@ struct EditMetadataView: View {
TopSection TopSection
Form { Form {
Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) {
let display_name_placeholder = "Satoshi Nakamoto" TextField("Satoshi Nakamoto", text: $display_name)
TextField(display_name_placeholder, text: $display_name)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
} }
Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) { Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) {
let username_placeholder = "satoshi" TextField("satoshi", text: $name)
TextField(username_placeholder, text: $name)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
+2 -2
View File
@@ -28,7 +28,7 @@ struct MaybeAnonPfpView: View {
.font(.largeTitle) .font(.largeTitle)
.frame(width: size, height: size) .frame(width: size, height: size)
} else { } 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) 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 { struct MaybeAnonPfpView_Previews: PreviewProvider {
static var previews: some View { 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)
} }
} }
-1
View File
@@ -92,7 +92,6 @@ struct InnerProfilePicView: View {
var Placeholder: some View { var Placeholder: some View {
Circle() Circle()
.frame(width: size, height: size) .frame(width: size, height: size)
.foregroundColor(DamusColors.mediumGrey)
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.padding(2) .padding(2)
} }
+170 -161
View File
@@ -46,38 +46,13 @@ func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String
return String(format: format, locale: locale, count) return String(format: format, locale: locale, count)
} }
func followedByString(_ friend_intersection: [String], profiles: Profiles, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
let names: [String] = friend_intersection.prefix(3).map {
let profile = profiles.lookup(id: $0)
return Profile.displayName(profile: profile, pubkey: $0).username.truncate(maxLength: 20)
}
switch friend_intersection.count {
case 0:
return ""
case 1:
let format = NSLocalizedString("Followed by %@", bundle: bundle, comment: "Text to indicate that the user is followed by one of our follows.")
return String(format: format, locale: locale, names[0])
case 2:
let format = NSLocalizedString("Followed by %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by two of our follows.")
return String(format: format, locale: locale, names[0], names[1])
case 3:
let format = NSLocalizedString("Followed by %@, %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by three of our follows.")
return String(format: format, locale: locale, names[0], names[1], names[2])
default:
let format = localizedStringFormat(key: "followed_by_three_and_others", locale: locale)
return String(format: format, locale: locale, friend_intersection.count - 3, names[0], names[1], names[2])
}
}
struct EditButton: View { struct EditButton: View {
let damus_state: DamusState let damus_state: DamusState
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var body: some View { var body: some View {
NavigationLink(value: Route.EditMetadata) { NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
Text("Edit", comment: "Button to edit user's profile.") Text("Edit", comment: "Button to edit user's profile.")
.frame(height: 30) .frame(height: 30)
.padding(.horizontal,25) .padding(.horizontal,25)
@@ -92,11 +67,11 @@ struct EditButton: View {
.lineLimit(1) .lineLimit(1)
} }
} }
func fillColor() -> Color { func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
func borderColor() -> Color { func borderColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
@@ -104,11 +79,11 @@ struct EditButton: View {
struct VisualEffectView: UIViewRepresentable { struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect? var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
UIVisualEffectView() UIVisualEffectView()
} }
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
uiView.effect = effect uiView.effect = effect
} }
@@ -118,54 +93,57 @@ struct ProfileView: View {
let damus_state: DamusState let damus_state: DamusState
let pfp_size: CGFloat = 90.0 let pfp_size: CGFloat = 90.0
let bannerHeight: CGFloat = 150.0 let bannerHeight: CGFloat = 150.0
let max_about_length = 280
static let markdown = Markdown() static let markdown = Markdown()
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false @State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false @State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false @State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false @State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts @State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0 @State var yOffset: CGFloat = 0
@State var show_full_about: Bool = false
@StateObject var profile: ProfileModel @StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel @StateObject var followers: FollowersModel
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel() @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) { init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state self.damus_state = damus_state
self._profile = StateObject(wrappedValue: profile) self._profile = StateObject(wrappedValue: profile)
self._followers = StateObject(wrappedValue: followers) self._followers = StateObject(wrappedValue: followers)
} }
init(damus_state: DamusState, pubkey: String) { init(damus_state: DamusState, pubkey: String) {
self.damus_state = damus_state self.damus_state = damus_state
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey)) self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
} }
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
func imageBorderColor() -> Color { func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? DamusColors.white : DamusColors.black
} }
func bannerBlurViewOpacity() -> Double { func bannerBlurViewOpacity() -> Double {
let progress = -(yOffset + navbarHeight) / 100 let progress = -(yOffset + navbarHeight) / 100
return Double(-yOffset > navbarHeight ? progress : 0) return Double(-yOffset > navbarHeight ? progress : 0)
} }
var bannerSection: some View { var bannerSection: some View {
GeometryReader { proxy -> AnyView in GeometryReader { proxy -> AnyView in
let minY = proxy.frame(in: .global).minY let minY = proxy.frame(in: .global).minY
DispatchQueue.main.async { DispatchQueue.main.async {
self.yOffset = minY self.yOffset = minY
} }
return AnyView( return AnyView(
VStack(spacing: 0) { VStack(spacing: 0) {
ZStack { ZStack {
@@ -173,10 +151,10 @@ struct ProfileView: View {
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped() .clipped()
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity()) VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
} }
Divider().opacity(bannerBlurViewOpacity()) Divider().opacity(bannerBlurViewOpacity())
} }
.frame(height: minY > 0 ? bannerHeight + minY : nil) .frame(height: minY > 0 ? bannerHeight + minY : nil)
@@ -187,11 +165,11 @@ struct ProfileView: View {
.frame(height: bannerHeight) .frame(height: bannerHeight)
.allowsHitTesting(false) .allowsHitTesting(false)
} }
var navbarHeight: CGFloat { var navbarHeight: CGFloat {
return 100.0 - (Theme.safeAreaInsets?.top ?? 0) return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
} }
@ViewBuilder @ViewBuilder
func navImage(img: String) -> some View { func navImage(img: String) -> some View {
Image(img) Image(img)
@@ -199,7 +177,7 @@ struct ProfileView: View {
.background(Color.black.opacity(0.6)) .background(Color.black.opacity(0.6))
.clipShape(Circle()) .clipShape(Circle())
} }
var navBackButton: some View { var navBackButton: some View {
Button { Button {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
@@ -207,7 +185,7 @@ struct ProfileView: View {
navImage(img: "chevron-left") navImage(img: "chevron-left")
} }
} }
var navActionSheetButton: some View { var navActionSheetButton: some View {
Button(action: { Button(action: {
action_sheet_presented = true action_sheet_presented = true
@@ -218,7 +196,7 @@ struct ProfileView: View {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) { Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true show_share_sheet = true
} }
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) { Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
show_qr_code = true show_qr_code = true
} }
@@ -238,7 +216,7 @@ struct ProfileView: View {
else { else {
return return
} }
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: profile.pubkey) else { guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: profile.pubkey) else {
return return
} }
@@ -254,7 +232,7 @@ struct ProfileView: View {
} }
} }
} }
var customNavbar: some View { var customNavbar: some View {
HStack { HStack {
navBackButton navBackButton
@@ -265,11 +243,11 @@ struct ProfileView: View {
.padding(.horizontal) .padding(.horizontal)
.accentColor(DamusColors.white) .accentColor(DamusColors.white)
} }
func lnButton(lnurl: String, profile: Profile) -> some View { func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap" let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: { return Button(action: {
present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) zap_button_model.showing_zap_customizer = true
}) { }) {
Image(button_img) Image(button_img)
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary) .foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
@@ -278,7 +256,7 @@ struct ProfileView: View {
if profile.reactions == false { 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.") 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 { if let addr = profile.lud16 {
Button { Button {
UIPasteboard.general.string = addr UIPasteboard.general.string = addr
@@ -293,30 +271,63 @@ struct ProfileView: View {
} }
} }
} }
} }
.cornerRadius(24) .cornerRadius(24)
.sheet(isPresented: $zap_button_model.showing_zap_customizer) {
CustomizeZapView(state: damus_state, target: ZapTarget.profile(self.profile.pubkey), lnurl: lnurl)
}
.sheet(isPresented: $zap_button_model.showing_select_wallet, onDismiss: {zap_button_model.showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $zap_button_model.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: zap_button_model.invoice ?? "")
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.target.id == self.profile.pubkey 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 {
zap_button_model.invoice = inv
zap_button_model.showing_select_wallet = true
} else {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
} }
var dmButton: some View { var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) 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") Image("messages")
.profile_button_style(scheme: colorScheme) .profile_button_style(scheme: colorScheme)
} }
} }
func actionSection(profile_data: Profile?) -> some View { func actionSection(profile_data: Profile?) -> some View {
return Group { return Group {
if let profile = profile_data { if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" { if let lnurl = profile.lnurl, lnurl != "" {
lnButton(lnurl: lnurl, profile: profile) lnButton(lnurl: lnurl, profile: profile)
} }
} }
dmButton dmButton
if profile.pubkey != damus_state.pubkey { if profile.pubkey != damus_state.pubkey {
FollowButtonView( FollowButtonView(
target: profile.get_follow_target(), target: profile.get_follow_target(),
@@ -324,26 +335,26 @@ struct ProfileView: View {
follow_state: damus_state.contacts.follow_state(profile.pubkey) follow_state: damus_state.contacts.follow_state(profile.pubkey)
) )
} else if damus_state.keypair.privkey != nil { } else if damus_state.keypair.privkey != nil {
NavigationLink(value: Route.EditMetadata) { NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
EditButton(damus_state: damus_state) EditButton(damus_state: damus_state)
} }
} }
} }
} }
func pfpOffset() -> CGFloat { func pfpOffset() -> CGFloat {
let progress = -yOffset / navbarHeight let progress = -yOffset / navbarHeight
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1) let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
return offset > 0 ? offset : 0 return offset > 0 ? offset : 0
} }
func pfpScale() -> CGFloat { func pfpScale() -> CGFloat {
let progress = -yOffset / navbarHeight let progress = -yOffset / navbarHeight
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1)) let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
return scale < 1 ? scale : 1 return scale < 1 ? scale : 1
} }
func nameSection(profile_data: Profile?) -> some View { func nameSection(profile_data: Profile?) -> some View {
return Group { return Group {
HStack(alignment: .center) { HStack(alignment: .center) {
@@ -357,17 +368,17 @@ struct ProfileView: View {
.fullScreenCover(isPresented: $is_zoomed) { .fullScreenCover(isPresented: $is_zoomed) {
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
} }
Spacer() Spacer()
actionSection(profile_data: profile_data) actionSection(profile_data: profile_data)
} }
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) 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) ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
} }
} }
var followersCount: some View { var followersCount: some View {
HStack { HStack {
if followers.count == nil { if followers.count == nil {
@@ -384,26 +395,47 @@ struct ProfileView: View {
} }
} }
} }
var aboutSection: some View { var aboutSection: some View {
VStack(alignment: .leading, spacing: 8.0) { VStack(alignment: .leading, spacing: 8.0) {
let profile_data = damus_state.profiles.lookup(id: profile.pubkey) let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
nameSection(profile_data: profile_data) nameSection(profile_data: profile_data)
if let about = profile_data?.about { 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
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)
}
if let url = profile_data?.website_url { if let url = profile_data?.website_url {
WebsiteLink(url: url) WebsiteLink(url: url)
} }
HStack { HStack {
if let contact = profile.contacts { if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id } let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(value: Route.Following(following: following_model)) { NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack { HStack {
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray) let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
@@ -411,9 +443,10 @@ struct ProfileView: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey)
.environmentObject(followers)
if followers.contacts != nil { if followers.contacts != nil {
NavigationLink(value: Route.Followers(followers: followers)) { NavigationLink(destination: fview) {
followersCount followersCount
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
@@ -425,108 +458,84 @@ struct ProfileView: View {
followers.subscribe() followers.subscribe()
} }
} }
if let relays = profile.relays { 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. // 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 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'.") 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 { if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(value: Route.RelayConfig) { NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text relay_text
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} else { } else {
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) { NavigationLink(destination: UserRelaysView(state: damus_state, relays: Array(relays.keys).sorted())) {
relay_text relay_text
} }
.buttonStyle(PlainButtonStyle()) .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) .padding(.horizontal)
} }
var body: some View { var body: some View {
ZStack { ScrollView(.vertical) {
ScrollView(.vertical) { VStack(spacing: 0) {
VStack(spacing: 0) { bannerSection
bannerSection .zIndex(1)
.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() { if filter_state == FilterState.posts {
aboutSection InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
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)
}
} }
.padding(.horizontal, Theme.safeAreaInsets?.left) if filter_state == FilterState.posts_and_replies {
.zIndex(-yOffset > navbarHeight ? 0 : 1) InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
}
}
.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])
} }
} }
.padding(.horizontal, Theme.safeAreaInsets?.left)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
} }
.fullScreenCover(isPresented: $show_qr_code) { }
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey) .ignoresSafeArea()
} .navigationTitle("")
.navigationBarHidden(true)
if damus_state.is_privkey_user { .overlay(customNavbar, alignment: .top)
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { .onReceive(handle_notify(.switched_timeline)) { _ in
notify(.compose, PostAction.posting(.user(profile.pubkey))) 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 +549,7 @@ struct ProfileView_Previews: PreviewProvider {
func test_damus_state() -> DamusState { func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState.empty 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 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) let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
damus.profiles.add(id: pubkey, profile: tsprof) damus.profiles.add(id: pubkey, profile: tsprof)
@@ -549,15 +558,15 @@ func test_damus_state() -> DamusState {
struct KeyView: View { struct KeyView: View {
let pubkey: String let pubkey: String
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@State private var isCopied = false @State private var isCopied = false
func keyColor() -> Color { func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
private func copyPubkey(_ pubkey: String) { private func copyPubkey(_ pubkey: String) {
UIPasteboard.general.string = pubkey UIPasteboard.general.string = pubkey
UIImpactFeedbackGenerator(style: .medium).impactOccurred() UIImpactFeedbackGenerator(style: .medium).impactOccurred()
@@ -570,10 +579,10 @@ struct KeyView: View {
} }
} }
} }
var body: some View { var body: some View {
let bech32 = bech32_pubkey(pubkey) ?? pubkey let bech32 = bech32_pubkey(pubkey) ?? pubkey
HStack { HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote) .font(.footnote)
@@ -581,7 +590,7 @@ struct KeyView: View {
.padding(5) .padding(5)
.padding([.leading, .trailing], 5) .padding([.leading, .trailing], 5)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey)) .background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
if isCopied != true { if isCopied != true {
Button { Button {
copyPubkey(bech32) copyPubkey(bech32)
+71 -242
View File
@@ -8,53 +8,11 @@
import SwiftUI import SwiftUI
import CoreImage.CIFilterBuiltins import CoreImage.CIFilterBuiltins
struct ProfileScanResult: Equatable {
let pubkey: String
init(hex: String) {
self.pubkey = hex
}
init?(string: String) {
var str = string
guard str.count != 0 else {
return nil
}
if str.hasPrefix("nostr:") {
str.removeFirst("nostr:".count)
}
if let _ = hex_decode(str), str.count == 64 {
self = .init(hex: str)
return
}
if str.starts(with: "npub"), let b32 = try? bech32_decode(str) {
let hex = hex_encode(b32.data)
self = .init(hex: hex)
return
}
return nil
}
}
struct QRCodeView: View { struct QRCodeView: View {
let damus_state: DamusState let damus_state: DamusState
@State var pubkey: String @State var pubkey: String
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@State private var selectedTab = 0
@State var scanResult: ProfileScanResult? = nil
@State var profile: Profile? = nil
@State var error: String? = nil
@State private var outerTrimEnd: CGFloat = 0
var animationDuration: Double = 0.5
let generator = UIImpactFeedbackGenerator(style: .light)
var maybe_key: String? { var maybe_key: String? {
guard let key = bech32_pubkey(pubkey) else { guard let key = bech32_pubkey(pubkey) else {
@@ -64,215 +22,87 @@ struct QRCodeView: View {
return key 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 { var body: some View {
NavigationView { ZStack(alignment: .center) {
ZStack(alignment: .center) {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
DamusGradient() DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image("close")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
} }
TabView(selection: $selectedTab) { .zIndex(1)
QRView
.tag(0)
if pubkey == damus_state.pubkey {
QRCameraView()
.tag(1)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.onAppear {
UIScrollView.appearance().isScrollEnabled = false
}
.gesture(
DragGesture()
.onChanged { _ in }
)
} }
}
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
}
var QRView: some View {
VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.padding(.top, 50)
}
if let display_name = profile?.display_name {
Text(display_name)
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.font(.body)
}
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.font(.system(size: 18, weight: .ultraLight))
Spacer()
Button(action: {
selectedTab = 1
}) {
HStack {
Text("Scan Code", comment: "Button to switch to scan QR Code page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(50)
}
}
func QRCameraView() -> some View {
return VStack(alignment: .center) {
Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.")
.padding(.top, 50)
.font(.system(size: 24, weight: .heavy))
Spacer()
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in
switch result {
case .success(let success):
handleProfileScan(success.string)
case .failure(let failure):
self.error = failure.localizedDescription
}
}
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
.rotationEffect(.degrees(-90)))
.shadow(radius: 10)
Spacer()
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 VStack(alignment: .center) {
guard result != self.scanResult else { let profile = damus_state.profiles.lookup(id: pubkey)
return
} 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)
generator.impactOccurred() .padding(.top, 50)
cameraAnimate { } else {
scanResult = result Image(systemName: "person.fill")
.font(.system(size: 60))
find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in .foregroundColor(DamusColors.white)
guard let res else { .padding(.top, 50)
error = "Profile not found"
return
} }
switch res { if let display_name = profile?.display_name {
case .invalid_profile: Text(display_name)
error = "Profile was found but was corrupt." .foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
case .profile: }
show_profile_after_delay() if let name = profile?.name {
Text("@" + name)
case .event: .foregroundColor(DamusColors.white)
print("invalid search result") .font(.body)
} }
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
if (pubkey == damus_state.pubkey) {
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
} else {
Text("Follow them on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user (someone else).")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
}
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.foregroundColor(DamusColors.white)
.font(.system(size: 18, weight: .ultraLight))
Spacer()
} }
} }
} .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
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()
} }
func generateQRCode(pubkey: String) -> UIImage { func generateQRCode(pubkey: String) -> UIImage {
@@ -300,4 +130,3 @@ struct QRCodeView_Previews: PreviewProvider {
QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey) QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
} }
} }
+3
View File
@@ -10,6 +10,9 @@ import SwiftUI
struct RelayFilterView: View { struct RelayFilterView: View {
let state: DamusState let state: DamusState
let timeline: Timeline let timeline: Timeline
//@State var relays: [RelayDescriptor]
//@EnvironmentObject var user_settings: UserSettingsStore
//@State var relays: [RelayDescriptor]
init(state: DamusState, timeline: Timeline) { init(state: DamusState, timeline: Timeline) {
self.state = state self.state = state
@@ -42,7 +42,9 @@ struct RecommendedRelayView: View {
Text(relay).layoutPriority(1) Text(relay).layoutPriority(1)
if let meta = damus.relay_metadata.lookup(relay_id: relay) { 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() EmptyView()
} }
.opacity(0.0) .opacity(0.0)
+6 -15
View File
@@ -73,18 +73,13 @@ struct RelayDetailView: View {
if let pubkey = nip11.pubkey { if let pubkey = nip11.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) { Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserViewRow(damus_state: state, pubkey: pubkey) UserViewRow(damus_state: state, pubkey: pubkey)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
}
} }
} }
if let relay_connection { Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) { HStack {
HStack { Text(relay)
Text(relay) Spacer()
Spacer() RelayStatus(pool: state.pool, relay: relay)
RelayStatusView(connection: relay_connection)
}
} }
} }
if nip11.is_paid { if nip11.is_paid {
@@ -93,7 +88,7 @@ struct RelayDetailView: View {
}, header: { }, header: {
Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.") Text("Paid Relay", comment: "Section header that indicates the relay server requires payment.")
}, footer: { }, footer: {
Text("This is a paid relay, you must pay for 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.")
}) })
} }
@@ -139,10 +134,6 @@ struct RelayDetailView: View {
} }
return attrString return attrString
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
}
} }
struct RelayDetailView_Previews: PreviewProvider { struct RelayDetailView_Previews: PreviewProvider {
+65
View File
@@ -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")
}
}
-33
View File
@@ -1,33 +0,0 @@
//
// RelayStatusView.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayStatusView: View {
@ObservedObject var connection: RelayConnection
var body: some View {
Group {
if connection.isConnecting {
ProgressView()
} else {
Image(connection.isConnected ? "globe" : "warning.fill")
.resizable()
.foregroundColor(connection.isConnected ? .green : .red)
}
}
.frame(width: 20, height: 20)
.padding(.trailing, 5)
}
}
struct RelayStatusView_Previews: PreviewProvider {
static var previews: some View {
let connection = test_damus_state().pool.get_relay("relay")!.connection
RelayStatusView(connection: connection)
}
}
+1 -7
View File
@@ -26,18 +26,12 @@ struct RelayToggle: View {
var body: some View { var body: some View {
HStack { HStack {
if let relay_connection { RelayStatus(pool: state.pool, relay: relay_id)
RelayStatusView(connection: relay_connection)
}
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false) RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false)
Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id)) Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id))
.toggleStyle(SwitchToggleStyle(tint: .accentColor)) .toggleStyle(SwitchToggleStyle(tint: .accentColor))
} }
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay_id)?.connection
}
} }
struct RelayToggle_Previews: PreviewProvider { struct RelayToggle_Previews: PreviewProvider {
+4 -9
View File
@@ -20,8 +20,8 @@ struct RelayView: View {
if showActionButtons { if showActionButtons {
RemoveButton(privkey: privkey, showText: false) RemoveButton(privkey: privkey, showText: false)
} }
else if let relay_connection { else {
RelayStatusView(connection: relay_connection) RelayStatus(pool: state.pool, relay: relay)
} }
} }
@@ -30,9 +30,8 @@ struct RelayView: View {
if let meta = state.relay_metadata.lookup(relay_id: relay) { if let meta = state.relay_metadata.lookup(relay_id: relay) {
Text(relay) Text(relay)
.background( .background(
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta), label: { NavigationLink("", destination: RelayDetailView(state: state, relay: relay, nip11: meta)).opacity(0.0)
EmptyView() .disabled(showActionButtons)
}).opacity(0.0).disabled(showActionButtons)
) )
Spacer() Spacer()
@@ -68,10 +67,6 @@ struct RelayView: View {
} }
} }
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
}
func CopyAction(relay: String) -> some View { func CopyAction(relay: String) -> some View {
Button { Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text") UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
+1 -1
View File
@@ -14,7 +14,7 @@ struct SignalView: View {
var body: some View { var body: some View {
Group { Group {
if signal.signal != signal.max_signal { if signal.signal != signal.max_signal {
NavigationLink(value: Route.RelayConfig) { NavigationLink(destination: RelayConfigView(state: state)) {
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.") Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout) .font(.callout)
.foregroundColor(.gray) .foregroundColor(.gray)
+2 -1
View File
@@ -16,8 +16,9 @@ struct RepostedEvent: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let prof = damus.profiles.lookup(id: event.pubkey) let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) { NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof) Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
.padding(.horizontal) .padding(.horizontal)
} }
+17 -11
View File
@@ -72,20 +72,24 @@ struct SearchingEventView: View {
} }
case .event: case .event:
find_event(state: state, query: .event(evid: evid)) { res in if let ev = state.events.lookup(evid) {
guard case .event(let ev) = res else {
self.search_state = .not_found
return
}
self.search_state = .found(ev) self.search_state = .found(ev)
return
}
find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { ev in
if let ev {
self.search_state = .found(ev)
} else {
self.search_state = .not_found
}
} }
case .profile: case .profile:
find_event(state: state, query: .profile(pubkey: evid)) { res in find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { ev in
guard case .profile(_, let ev) = res else { if state.profiles.lookup(id: evid) != nil {
self.search_state = .found_profile(evid)
} else {
self.search_state = .not_found self.search_state = .not_found
return
} }
self.search_state = .found_profile(ev.pubkey)
} }
} }
} }
@@ -100,12 +104,14 @@ struct SearchingEventView: View {
.progressViewStyle(.circular) .progressViewStyle(.circular)
} }
case .found(let ev): case .found(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) EventView(damus: state, event: ev)
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
case .found_profile(let pk): case .found_profile(let pk):
NavigationLink(value: Route.ProfileByKey(pubkey: pk)) { NavigationLink(destination: ProfileView(damus_state: state, pubkey: pk)) {
FollowUserView(target: .pubkey(pk), damus_state: state) FollowUserView(target: .pubkey(pk), damus_state: state)
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
+4 -1
View File
@@ -126,6 +126,9 @@ struct SearchHomeView: View {
struct SearchHomeView_Previews: PreviewProvider { struct SearchHomeView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let state = test_damus_state() let state = test_damus_state()
SearchHomeView(damus_state: state, model: SearchHomeModel(damus_state: state)) SearchHomeView(
damus_state: state,
model: SearchHomeModel(damus_state: state)
)
} }
} }
+2 -1
View File
@@ -44,7 +44,8 @@ struct InnerSearchResults: View {
func HashtagSearch(_ ht: String) -> some View { func HashtagSearch(_ ht: String) -> some View {
let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht])) let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht]))
return NavigationLink(value: Route.Search(search: search_model)) { let dst = SearchView(appstate: damus_state, search: search_model)
return NavigationLink(destination: dst) {
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.") Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
} }
} }
+4 -4
View File
@@ -9,7 +9,7 @@ import SwiftUI
struct SelectWalletView: View { struct SelectWalletView: View {
let default_wallet: Wallet let default_wallet: Wallet
@Binding var active_sheet: Sheets? @Binding var showingSelectWallet: Bool
let our_pubkey: String let our_pubkey: String
let invoice: String let invoice: String
@State var invoice_copied: Bool = false @State var invoice_copied: Bool = false
@@ -59,7 +59,7 @@ struct SelectWalletView: View {
}.padding(.vertical, 2.5) }.padding(.vertical, 2.5)
} }
}.navigationBarTitle(Text("Pay the Lightning invoice", comment: "Navigation bar title for view to pay Lightning invoice."), displayMode: .inline).navigationBarItems(trailing: Button(action: { }.navigationBarTitle(Text("Pay the Lightning invoice", comment: "Navigation bar title for view to pay Lightning invoice."), displayMode: .inline).navigationBarItems(trailing: Button(action: {
self.active_sheet = nil self.showingSelectWallet = false
}) { }) {
Text("Done", comment: "Button to dismiss wallet selection view for paying Lightning invoice.").bold() Text("Done", comment: "Button to dismiss wallet selection view for paying Lightning invoice.").bold()
}) })
@@ -68,9 +68,9 @@ struct SelectWalletView: View {
} }
struct SelectWalletView_Previews: PreviewProvider { struct SelectWalletView_Previews: PreviewProvider {
@State static var active_sheet: Sheets? = nil @State static var show: Bool = true
static var previews: some View { static var previews: some View {
SelectWalletView(default_wallet: .lnlink, active_sheet: $active_sheet, our_pubkey: "", invoice: "") SelectWalletView(default_wallet: .lnlink, showingSelectWallet: $show, our_pubkey: "", invoice: "")
} }
} }
+25 -19
View File
@@ -17,12 +17,16 @@ func hex_col(r: UInt8, g: UInt8, b: UInt8) -> Color {
struct SetupView: View { struct SetupView: View {
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() @State private var eula = false
var body: some View { var body: some View {
NavigationStack(path: $navigationCoordinator.path) { NavigationView {
ZStack { ZStack {
VStack(alignment: .center) { VStack(alignment: .center) {
NavigationLink(destination: EULAView(), isActive: $eula) {
EmptyView()
}
Spacer() Spacer()
Image("logo-nobg") Image("logo-nobg")
@@ -30,13 +34,18 @@ struct SetupView: View {
.shadow(color: DamusColors.purple, radius: 2) .shadow(color: DamusColors.purple, radius: 2)
.frame(width: 56, height: 56, alignment: .center) .frame(width: 56, height: 56, alignment: .center)
.padding(.top, 20.0) .padding(.top, 20.0)
Text("Welcome to Damus", comment: "Welcome text shown on the first screen when user is not logged in.") HStack {
.font(.title) Text("Welcome to", comment: "Welcome text shown on the first screen when user is not logged in.")
.fontWeight(.heavy) .font(.title)
.foregroundStyle(DamusLogoGradient.gradient) .fontWeight(.heavy)
Text("Damus")
Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is") .font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
}
Text("The go-to iOS nostr client", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
.padding(.top, 10) .padding(.top, 10)
@@ -49,10 +58,10 @@ struct SetupView: View {
Spacer() Spacer()
Button(action: { Button(action: {
navigationCoordinator.push(route: Route.EULA) eula.toggle()
}) { }) {
HStack { HStack {
Text("Let's get started!", comment: "Button to continue to login page.") Text("Let's get started!", comment: "Button to continue to login page.")
.fontWeight(.semibold) .fontWeight(.semibold)
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
@@ -68,9 +77,6 @@ struct SetupView: View {
.ignoresSafeArea(), .ignoresSafeArea(),
alignment: .top alignment: .top
) )
.navigationDestination(for: Route.self) { route in
route.view(navigationCordinator: navigationCoordinator, damusState: DamusState.empty)
}
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
@@ -84,7 +90,7 @@ struct LearnAboutNostrLink: View {
Button(action: { Button(action: {
openURL(URL(string: "https://nostr.com")!) openURL(URL(string: "https://nostr.com")!)
}, label: { }, label: {
Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.") Text("Learn more about nostr")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
}) })
@@ -100,11 +106,11 @@ struct WhatIsNostr: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Image("nostr-logo") Image("nostr-logo")
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.") Text("What is nostr?")
.fontWeight(.bold) .fontWeight(.bold)
.padding(.vertical, 10) .padding(.vertical, 10)
Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.") Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
LearnAboutNostrLink() LearnAboutNostrLink()
@@ -119,11 +125,11 @@ struct WhyWeNeedNostr: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Image("lightbulb") Image("lightbulb")
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.") Text("Why we need nostr?")
.fontWeight(.bold) .fontWeight(.bold)
.padding(.vertical, 10) .padding(.vertical, 10)
Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.") Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken")
.foregroundColor(DamusColors.mediumGrey) .foregroundColor(DamusColors.mediumGrey)
} }
} }
+36 -34
View File
@@ -11,22 +11,23 @@ struct SideMenuView: View {
let damus_state: DamusState let damus_state: DamusState
@Binding var isSidebarVisible: Bool @Binding var isSidebarVisible: Bool
@State var confirm_logout: Bool = false @State var confirm_logout: Bool = false
@State private var showQRCode = false @State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0) var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0)
let verticalSpacing: CGFloat = 20 let verticalSpacing: CGFloat = 20
let padding: CGFloat = 30 let padding: CGFloat = 30
func fillColor() -> Color { func fillColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? DamusColors.white : DamusColors.black
} }
func textColor() -> Color { func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? DamusColors.black : DamusColors.white
} }
var body: some View { var body: some View {
ZStack { ZStack {
GeometryReader { _ in GeometryReader { _ in
@@ -41,20 +42,20 @@ struct SideMenuView: View {
content content
} }
} }
func SidemenuItems(profile_model: ProfileModel, followers: FollowersModel) -> some View { func SidemenuItems(profile_model: ProfileModel, followers: FollowersModel) -> some View {
return VStack(spacing: verticalSpacing) { return VStack(spacing: verticalSpacing) {
NavigationLink(value: Route.Profile(profile: profile_model, followers: followers)) { NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user") navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user")
} }
NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) { NavigationLink(destination: WalletView(damus_state: damus_state, model: damus_state.wallet)) {
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet") navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet")
/* /*
HStack { HStack {
Image("wallet") Image("wallet")
.tint(DamusColors.adaptableBlack) .tint(DamusColors.adaptableBlack)
Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view.")) Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view."))
.font(.title2) .font(.title2)
.foregroundColor(textColor()) .foregroundColor(textColor())
@@ -62,35 +63,36 @@ struct SideMenuView: View {
.dynamicTypeSize(.xSmall) .dynamicTypeSize(.xSmall)
}*/ }*/
} }
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) { NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute") navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
} }
NavigationLink(value: Route.RelayConfig) { NavigationLink(destination: RelayConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), img: "world-relays") navLabel(title: NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), img: "world-relays")
} }
NavigationLink(value: Route.Bookmarks) { NavigationLink(destination: BookmarksView(state: damus_state)) {
navLabel(title: NSLocalizedString("Bookmarks", comment: "Sidebar menu label for Bookmarks view."), img: "bookmark") navLabel(title: NSLocalizedString("Bookmarks", comment: "Sidebar menu label for Bookmarks view."), img: "bookmark")
} }
NavigationLink(value: Route.Config) { NavigationLink(destination: ConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), img: "settings") navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), img: "settings")
} }
} }
} }
var MainSidemenu: some View { var MainSidemenu: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey) let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey) let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey)
let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state) let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state)
NavigationLink(value: Route.Profile(profile: profile_model, followers: followers), label: { NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
HStack { HStack {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let display_name = profile?.display_name { if let display_name = profile?.display_name {
Text(display_name) Text(display_name)
@@ -107,10 +109,10 @@ struct SideMenuView: View {
} }
} }
.padding(.bottom, verticalSpacing) .padding(.bottom, verticalSpacing)
}) }
Divider() Divider()
ScrollView { ScrollView {
SidemenuItems(profile_model: profile_model, followers: followers) SidemenuItems(profile_model: profile_model, followers: followers)
.labelStyle(SideMenuLabelStyle()) .labelStyle(SideMenuLabelStyle())
@@ -118,21 +120,21 @@ struct SideMenuView: View {
} }
} }
} }
var content: some View { var content: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
ZStack(alignment: .top) { ZStack(alignment: .top) {
fillColor() fillColor()
.ignoresSafeArea() .ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
MainSidemenu MainSidemenu
.simultaneousGesture(TapGesture().onEnded { .simultaneousGesture(TapGesture().onEnded {
isSidebarVisible = false isSidebarVisible = false
}) })
Divider() Divider()
HStack() { HStack() {
Button(action: { Button(action: {
//ConfigView(state: damus_state) //ConfigView(state: damus_state)
@@ -148,9 +150,9 @@ struct SideMenuView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall) .dynamicTypeSize(.xSmall)
}) })
Spacer() Spacer()
Button(action: { Button(action: {
showQRCode.toggle() showQRCode.toggle()
}, label: { }, label: {
@@ -184,20 +186,20 @@ struct SideMenuView: View {
Spacer() Spacer()
} }
} }
@ViewBuilder @ViewBuilder
func navLabel(title: String, img: String) -> some View { func navLabel(title: String, img: String) -> some View {
Image(img) Image(img)
.tint(DamusColors.adaptableBlack) .tint(DamusColors.adaptableBlack)
Text(title) Text(title)
.font(.title2) .font(.title2)
.foregroundColor(textColor()) .foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall) .dynamicTypeSize(.xSmall)
} }
struct SideMenuLabelStyle: LabelStyle { struct SideMenuLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 8) { HStack(alignment: .center, spacing: 8) {
+10 -14
View File
@@ -70,21 +70,17 @@ struct TextViewWrapper: UIViewRepresentable {
} }
private func processFocusedWordForMention(textView: UITextView) { private func processFocusedWordForMention(textView: UITextView) {
var val: (String?, NSRange?) = (nil, nil) if let selectedRange = textView.selectedTextRange {
var val: (String?, NSRange?)
guard let selectedRange = textView.selectedTextRange else { return } if let wordRange = textView.tokenizer.rangeEnclosingPosition(selectedRange.start, with: .word, inDirection: .init(rawValue: UITextLayoutDirection.left.rawValue)) {
if let startPosition = textView.position(from: wordRange.start, offset: -1),
let wordRange = textView.tokenizer.rangeEnclosingPosition(selectedRange.start, with: .word, inDirection: .init(rawValue: UITextLayoutDirection.left.rawValue)) let cursorPosition = textView.position(from: selectedRange.start, offset: 0) {
let word = textView.text(in: textView.textRange(from: startPosition, to: cursorPosition)!)
if let wordRange, val = (word, convertToNSRange(startPosition, cursorPosition, textView))
let startPosition = textView.position(from: wordRange.start, offset: -1), }
let cursorPosition = textView.position(from: selectedRange.start, offset: 0) }
{ getFocusWordForMention?(val.0, val.1)
let word = textView.text(in: textView.textRange(from: startPosition, to: cursorPosition)!)
val = (word, convertToNSRange(startPosition, cursorPosition, textView))
} }
getFocusWordForMention?(val.0, val.1)
} }
private func convertToNSRange( _ startPosition: UITextPosition, _ endPosition: UITextPosition, _ textView: UITextView) -> NSRange? { private func convertToNSRange( _ startPosition: UITextPosition, _ endPosition: UITextPosition, _ textView: UITextView) -> NSRange? {
+2 -12
View File
@@ -10,7 +10,7 @@ import SwiftUI
struct ThreadView: View { struct ThreadView: View {
let state: DamusState let state: DamusState
@ObservedObject var thread: ThreadModel @StateObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var parent_events: [NostrEvent] { var parent_events: [NostrEvent] {
@@ -22,13 +22,11 @@ struct ThreadView: View {
} }
var body: some View { var body: some View {
//let top_zap = get_top_zap(events: state.events, evid: thread.event.id)
ScrollViewReader { reader in ScrollViewReader { reader in
ScrollView { ScrollView {
LazyVStack { LazyVStack {
// MARK: - Parents events view // MARK: - Parents events view
ForEach(parent_events, id: \.id) { parent_event in ForEach(parent_events, id: \.id) { parent_event in
MutedEventView(damus_state: state, MutedEventView(damus_state: state,
event: parent_event, event: parent_event,
selected: false) selected: false)
@@ -41,7 +39,6 @@ struct ThreadView: View {
Divider() Divider()
.padding(.top, 4) .padding(.top, 4)
.padding(.leading, 25 * 2) .padding(.leading, 25 * 2)
}.background(GeometryReader { geometry in }.background(GeometryReader { geometry in
// get the height and width of the EventView view // get the height and width of the EventView view
let eventHeight = geometry.frame(in: .global).height let eventHeight = geometry.frame(in: .global).height
@@ -62,13 +59,6 @@ struct ThreadView: View {
) )
.id(self.thread.event.id) .id(self.thread.event.id)
/*
if let top_zap {
ZapEvent(damus: state, zap: top_zap, is_top_zap: true)
.padding(.horizontal)
}
*/
ForEach(child_events, id: \.id) { child_event in ForEach(child_events, id: \.id) { child_event in
MutedEventView( MutedEventView(
damus_state: state, damus_state: state,
@@ -80,7 +70,7 @@ struct ThreadView: View {
thread.set_active_event(child_event) thread.set_active_event(child_event)
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false) scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
} }
Divider() Divider()
.padding([.top], 4) .padding([.top], 4)
} }
+11 -7
View File
@@ -12,15 +12,15 @@ struct InnerTimelineView: View {
@ObservedObject var events: EventHolder @ObservedObject var events: EventHolder
let state: DamusState let state: DamusState
let filter: (NostrEvent) -> Bool let filter: (NostrEvent) -> Bool
@State var nav_target: NostrEvent
static var count: Int = 0 @State var navigating: Bool = false
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool) { init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool) {
self.events = events self.events = events
self.state = damus self.state = damus
self.filter = filter self.filter = filter
print("rendering InnerTimelineView \(InnerTimelineView.count)") // dummy event to avoid MaybeThreadView
InnerTimelineView.count += 1 self._nav_target = State(initialValue: test_event)
} }
var event_options: EventViewOptions { var event_options: EventViewOptions {
@@ -32,6 +32,11 @@ struct InnerTimelineView: View {
} }
var body: some View { var body: some View {
let thread = ThreadModel(event: nav_target, damus_state: state)
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
let events = self.events.events let events = self.events.events
if events.isEmpty { if events.isEmpty {
@@ -44,9 +49,8 @@ struct InnerTimelineView: View {
let ind = tup.1 let ind = tup.1
EventView(damus: state, event: ev, options: event_options) EventView(damus: state, event: ev, options: event_options)
.onTapGesture { .onTapGesture {
let event = ev.get_inner_event(cache: state.events) ?? ev nav_target = ev.get_inner_event(cache: state.events) ?? ev
let thread = ThreadModel(event: event, damus_state: state) navigating = true
state.nav.push(route: Route.Thread(thread: thread))
} }
.padding(.top, 7) .padding(.top, 7)
.onAppear { .onAppear {
+3 -1
View File
@@ -31,7 +31,9 @@ struct TimelineView: View {
.shimmer(loading) .shimmer(loading)
.disabled(loading) .disabled(loading)
.background(GeometryReader { proxy -> Color in .background(GeometryReader { proxy -> Color in
handle_scroll_queue(proxy, queue: self.events) DispatchQueue.main.async {
handle_scroll_queue(proxy, queue: self.events)
}
return Color.clear return Color.clear
}) })
} }
+16 -25
View File
@@ -43,34 +43,25 @@ struct DamusVideoPlayer: View {
} }
var body: some View { var body: some View {
GeometryReader { geo in ZStack(alignment: .bottomTrailing) {
let localFrame = geo.frame(in: .local) VideoPlayer(url: url, model: model)
let localCenter = CGPoint(x: localFrame.midX, y: localFrame.midY) .onAppear {
let globalCenter = geo.frame(in: .global).origin.applying(.init(translationX: localCenter.x, y: localCenter.y)) model.start()
let centerY = globalCenter.y
ZStack(alignment: .bottomTrailing) {
VideoPlayer(url: url, model: model)
if model.has_audio == true {
MuteIcon
.zIndex(11.0)
.onTapGesture {
self.model.muted = !self.model.muted
}
} }
if model.has_audio == true {
MuteIcon
.zIndex(11.0)
.onTapGesture {
self.model.muted = !self.model.muted
}
} }
.onChange(of: model.size) { size in }
guard let size else { .onChange(of: model.size) { size in
return guard let size else {
} return
video_size = size
}
.onChange(of: centerY) { _ in
let screenHeight = UIScreen.main.bounds.height
let screenMidY = screenHeight / 2
let tol = 0.20 * screenHeight /// tolerance - can vary to taste ie., % of screen height of a centered box in which video plays
model.play = centerY > screenMidY - tol && centerY < screenMidY + tol /// video plays when inside tolerance box
} }
video_size = size
} }
} }
} }
+14 -17
View File
@@ -39,7 +39,6 @@ enum VideoHandler {
case onStateChanged((VideoState) -> Void) case onStateChanged((VideoState) -> Void)
} }
@MainActor
public class VideoPlayerModel: ObservableObject { public class VideoPlayerModel: ObservableObject {
@Published var autoReplay: Bool = true @Published var autoReplay: Bool = true
@Published var muted: Bool = true @Published var muted: Bool = true
@@ -48,8 +47,7 @@ public class VideoPlayerModel: ObservableObject {
@Published var has_audio: Bool? = nil @Published var has_audio: Bool? = nil
@Published var contentMode: UIView.ContentMode = .scaleAspectFill @Published var contentMode: UIView.ContentMode = .scaleAspectFill
fileprivate var time: CMTime? var time: CMTime = CMTime()
var handlers: [VideoHandler] = [] var handlers: [VideoHandler] = []
init() { init() {
@@ -166,11 +164,15 @@ public extension VideoPlayer {
} }
} }
func get_video_size(player: AVPlayer) async -> CGSize? { @available(iOS 13, *)
let res = Task.detached(priority: .background) { public extension VideoPlayer {
return player.currentImage?.size
}
return await res.value }
func get_video_size(player: AVPlayer) -> CGSize? {
// TODO: make this async?
return player.currentImage?.size
} }
func video_has_audio(player: AVPlayer) async -> Bool { func video_has_audio(player: AVPlayer) async -> Bool {
@@ -218,7 +220,7 @@ extension VideoPlayer: UIViewRepresentable {
if let player = uiView.player { if let player = uiView.player {
Task { Task {
let has_audio = await video_has_audio(player: player) let has_audio = await video_has_audio(player: player)
let size = await get_video_size(player: player) let size = get_video_size(player: player)
Task { @MainActor in Task { @MainActor in
if let size { if let size {
self.model.size = size self.model.size = size
@@ -263,9 +265,8 @@ extension VideoPlayer: UIViewRepresentable {
uiView.isMuted = model.muted uiView.isMuted = model.muted
uiView.isAutoReplay = model.autoReplay uiView.isAutoReplay = model.autoReplay
if let observerTime = context.coordinator.observerTime, let modelTime = model.time, if let observerTime = context.coordinator.observerTime, model.time != observerTime {
modelTime != observerTime && modelTime.isValid && modelTime.isNumeric { uiView.seek(to: model.time, toleranceBefore: model.time, toleranceAfter: model.time, completion: { _ in })
uiView.seek(to: modelTime, completion: { _ in })
} }
} }
@@ -284,16 +285,13 @@ extension VideoPlayer: UIViewRepresentable {
self.videoPlayer = videoPlayer self.videoPlayer = videoPlayer
} }
@MainActor
func startObserver(uiView: VideoPlayerView) { func startObserver(uiView: VideoPlayerView) {
guard observer == nil else { return } guard observer == nil else { return }
observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in
guard let `self` = self else { return } guard let `self` = self else { return }
Task { @MainActor in self.videoPlayer.model.time = time
self.videoPlayer.model.time = time
}
self.observerTime = time self.observerTime = time
self.updateBuffer(uiView: uiView) self.updateBuffer(uiView: uiView)
@@ -315,7 +313,6 @@ extension VideoPlayer: UIViewRepresentable {
self.observerBuffer = nil self.observerBuffer = nil
} }
@MainActor
func updateBuffer(uiView: VideoPlayerView) { func updateBuffer(uiView: VideoPlayerView) {
let bufferProgress = uiView.bufferProgress let bufferProgress = uiView.bufferProgress
guard bufferProgress != observerBuffer else { return } guard bufferProgress != observerBuffer else { return }
+7 -4
View File
@@ -14,7 +14,6 @@ struct ConnectWalletView: View {
@State var scanning: Bool = false @State var scanning: Bool = false
@State var error: String? = nil @State var error: String? = nil
@State var wallet_scan_result: WalletScanResult = .scanning @State var wallet_scan_result: WalletScanResult = .scanning
var nav: NavigationCoordinator
var body: some View { var body: some View {
MainContent MainContent
@@ -64,13 +63,17 @@ struct ConnectWalletView: View {
} }
var ConnectWallet: some View { var ConnectWallet: some View {
VStack { VStack {
NavigationLink(destination: WalletScannerView(result: $wallet_scan_result), isActive: $scanning) {
EmptyView()
}
AlbyButton() { AlbyButton() {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!) openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
} }
BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) { BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
nav.push(route: Route.WalletScanner(result: $wallet_scan_result)) scanning = true
} }
if let err = self.error { if let err = self.error {
@@ -96,6 +99,6 @@ struct ConnectWalletView: View {
struct ConnectWalletView_Previews: PreviewProvider { struct ConnectWalletView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init()) ConnectWalletView(model: WalletModel(settings: UserSettingsStore()))
} }
} }
+1 -1
View File
@@ -67,7 +67,7 @@ struct NWCPaste: View {
}) { }) {
HStack { HStack {
Image(systemName: "doc.on.clipboard") Image(systemName: "doc.on.clipboard")
Text("Paste", comment: "Button to paste a Nostr Wallet Connect string to connect the wallet for use in Damus for zaps.") Text("Paste")
} }
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white) .foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
+5 -7
View File
@@ -20,11 +20,9 @@ struct WalletView: View {
func MainWalletView(nwc: WalletConnectURL) -> some View { func MainWalletView(nwc: WalletConnectURL) -> some View {
VStack { VStack {
if !damus_state.settings.nozaps { SupportDamus
SupportDamus
Spacer()
Spacer()
}
Text(verbatim: nwc.relay.id) Text(verbatim: nwc.relay.id)
@@ -155,9 +153,9 @@ struct WalletView: View {
var body: some View { var body: some View {
switch model.connect_state { switch model.connect_state {
case .new: case .new:
ConnectWalletView(model: model, nav: damus_state.nav) ConnectWalletView(model: model)
case .none: case .none:
ConnectWalletView(model: model, nav: damus_state.nav) ConnectWalletView(model: model)
case .existing(let nwc): case .existing(let nwc):
MainWalletView(nwc: nwc) MainWalletView(nwc: nwc)
.onAppear() { .onAppear() {
+96 -74
View File
@@ -48,10 +48,18 @@ struct CustomizeZapView: View {
let state: DamusState let state: DamusState
let target: ZapTarget let target: ZapTarget
let lnurl: String let lnurl: String
@State var comment: String
@State var custom_amount: String
@State var custom_amount_sats: Int?
@State var zap_type: ZapType
@State var invoice: String
@State var error: String?
@State var showing_wallet_selector: Bool
@State var zapping: Bool
@State var show_zap_types: Bool = false
let zap_amounts: [ZapAmountItem] let zap_amounts: [ZapAmountItem]
@StateObject var model: CustomizeZapModel = CustomizeZapModel()
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -64,12 +72,21 @@ struct CustomizeZapView: View {
} }
init(state: DamusState, target: ZapTarget, lnurl: String) { init(state: DamusState, target: ZapTarget, lnurl: String) {
self._comment = State(initialValue: "")
self.target = target self.target = target
self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount) self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
self._showing_wallet_selector = State(initialValue: false)
self._zap_type = State(initialValue: state.settings.default_zap_type)
self._custom_amount = State(initialValue: String(state.settings.default_zap_amount))
self._custom_amount_sats = State(initialValue: nil)
self._zapping = State(initialValue: false)
self.lnurl = lnurl self.lnurl = lnurl
self.state = state self.state = state
} }
func amount_parts(_ n: Int) -> [ZapAmountItem] { func amount_parts(_ n: Int) -> [ZapAmountItem] {
var i: Int = -1 var i: Int = -1
let start = n * 3 let start = n * 3
@@ -84,10 +101,7 @@ struct CustomizeZapView: View {
func AmountsPart(n: Int) -> some View { func AmountsPart(n: Int) -> some View {
HStack(alignment: .center, spacing: 15) { HStack(alignment: .center, spacing: 15) {
ForEach(amount_parts(n)) { entry in ForEach(amount_parts(n)) { entry in
ZapAmountButton(zapAmountItem: entry, action: { ZapAmountButton(zapAmountItem: entry, action: {custom_amount_sats = entry.amount; custom_amount = String(entry.amount)})
model.custom_amount_sats = entry.amount
model.custom_amount = String(entry.amount)
})
} }
} }
} }
@@ -111,17 +125,17 @@ struct CustomizeZapView: View {
.font(.headline) .font(.headline)
.frame(width: 70, height: 70) .frame(width: 70, height: 70)
.foregroundColor(fontColor()) .foregroundColor(fontColor())
.background(model.custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey) .background(custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey)
.cornerRadius(15) .cornerRadius(15)
.overlay(RoundedRectangle(cornerRadius: 15) .overlay(RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.purple.opacity(model.custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2)) .stroke(DamusColors.purple.opacity(custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2))
} }
} }
var CustomZapTextField: some View { var CustomZapTextField: some View {
VStack(alignment: .center, spacing: 0) { VStack(alignment: .center, spacing: 0) {
TextField("", text: $model.custom_amount) TextField("", text: $custom_amount)
.placeholder(when: model.custom_amount.isEmpty, alignment: .center) { .placeholder(when: custom_amount.isEmpty, alignment: .center) {
Text(verbatim: 0.formatted()) Text(verbatim: 0.formatted())
} }
.accentColor(.clear) .accentColor(.clear)
@@ -129,16 +143,16 @@ struct CustomizeZapView: View {
.minimumScaleFactor(0.01) .minimumScaleFactor(0.01)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.onChange(of: model.custom_amount) { newValue in .onReceive(Just(custom_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) { if let parsed = handle_string_amount(new_value: newValue) {
model.custom_amount = parsed.formatted() self.custom_amount = parsed.formatted()
model.custom_amount_sats = parsed self.custom_amount_sats = parsed
} else { } else {
model.custom_amount = "" self.custom_amount = ""
model.custom_amount_sats = nil self.custom_amount_sats = nil
} }
} }
Text(verbatim: satsString(model.custom_amount_sats ?? 0)) Text(verbatim: satsString(custom_amount_sats ?? 0))
.font(.system(size: 18, weight: .heavy)) .font(.system(size: 18, weight: .heavy))
} }
} }
@@ -146,12 +160,12 @@ struct CustomizeZapView: View {
var ZapReply: some View { var ZapReply: some View {
HStack { HStack {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
TextField(NSLocalizedString("Send a message with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $model.comment, axis: .vertical) TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment, axis: .vertical)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.lineLimit(5) .lineLimit(5)
} else { } else {
TextField(NSLocalizedString("Send a message with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $model.comment) TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
} }
@@ -165,24 +179,24 @@ struct CustomizeZapView: View {
var ZapButton: some View { var ZapButton: some View {
VStack { VStack {
if model.zapping { if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.") Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else { } else {
Button(NSLocalizedString("Zap User", comment: "Button to send a zap.")) { Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = model.custom_amount_sats let amount = custom_amount_sats
send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: model.comment, amount_sats: amount, zap_type: model.zap_type) send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
model.zapping = true self.zapping = true
} }
.disabled(model.custom_amount_sats == 0 || model.custom_amount.isEmpty) .disabled(custom_amount_sats == 0 || custom_amount.isEmpty)
.font(.system(size: 28, weight: .bold)) .font(.system(size: 28, weight: .bold))
.frame(width: 180, height: 50) .frame(width: 130, height: 50)
.foregroundColor(.white) .foregroundColor(.white)
.background(LINEAR_GRADIENT) .background(LINEAR_GRADIENT)
.opacity(model.custom_amount_sats == 0 || model.custom_amount.isEmpty ? 0.5 : 1.0) .opacity(custom_amount_sats == 0 || custom_amount.isEmpty ? 0.5 : 1.0)
.clipShape(Capsule()) .clipShape(Capsule())
} }
if let error = model.error { if let error {
Text(error) Text(error)
.foregroundColor(.red) .foregroundColor(.red)
} }
@@ -198,83 +212,56 @@ struct CustomizeZapView: View {
return return
} }
model.zapping = false self.zapping = false
switch zap_ev.type { switch zap_ev.type {
case .failed(let err): case .failed(let err):
switch err { switch err {
case .fetching_invoice: case .fetching_invoice:
model.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") self.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
case .bad_lnurl: case .bad_lnurl:
model.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") self.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
case .canceled: case .canceled:
model.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.") self.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
case .send_failed: case .send_failed:
model.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.") self.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
} }
break break
case .got_zap_invoice(let inv): case .got_zap_invoice(let inv):
if state.settings.show_wallet_selector { if state.settings.show_wallet_selector {
model.invoice = inv self.invoice = inv
present_sheet(.select_wallet(invoice: inv)) self.showing_wallet_selector = true
} else { } else {
end_editing() end_editing()
let wallet = state.settings.default_wallet.model let wallet = state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv) open_with_wallet(wallet: wallet, invoice: inv)
self.showing_wallet_selector = false
dismiss() dismiss()
} }
case .sent_from_nwc: case .sent_from_nwc:
dismiss() dismiss()
} }
} }
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 20) { MainContent
ScrollView { .sheet(isPresented: $showing_wallet_selector) {
HStack(alignment: .center) { SelectWalletView(default_wallet: state.settings.default_wallet, showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice)
UserView(damus_state: state, pubkey: target.pubkey)
ZapTypeButton()
}
.padding([.horizontal, .top])
CustomZapTextField
AmountPicker
ZapReply
ZapButton
Spacer()
} }
} .onReceive(handle_notify(.zapping)) { notif in
.sheet(isPresented: $model.show_zap_types) { receive_zap(notif: notif)
if #available(iOS 16.0, *) { }
ZapPicker .background(fillColor().edgesIgnoringSafeArea(.all))
.presentationDetents([.medium]) .onTapGesture {
.presentationDragIndicator(.visible) hideKeyboard()
} else {
ZapPicker
} }
}
.onAppear {
model.set_defaults(settings: state.settings)
}
.onReceive(handle_notify(.zapping)) { notif in
receive_zap(notif: notif)
}
.background(fillColor().edgesIgnoringSafeArea(.all))
.onTapGesture {
hideKeyboard()
}
} }
func ZapTypeButton() -> some View { func ZapTypeButton() -> some View {
Button(action: { Button(action: {
model.show_zap_types = true show_zap_types = true
}) { }) {
switch model.zap_type { switch zap_type {
case .pub: case .pub:
Image("globe") Image("globe")
Text("Public", comment: "Button text to indicate that the zap type is a public zap.") Text("Public", comment: "Button text to indicate that the zap type is a public zap.")
@@ -296,8 +283,43 @@ struct CustomizeZapView: View {
.cornerRadius(15) .cornerRadius(15)
} }
var CustomZap: some View {
VStack(alignment: .center, spacing: 20) {
ZapTypeButton()
.padding(.top, 50)
Spacer()
CustomZapTextField
AmountPicker
ZapReply
ZapButton
Spacer()
Spacer()
}
.sheet(isPresented: $show_zap_types) {
if #available(iOS 16.0, *) {
ZapPicker
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
} else {
ZapPicker
}
}
}
var ZapPicker: some View { var ZapPicker: some View {
ZapTypePicker(zap_type: $model.zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey) ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey)
}
var MainContent: some View {
CustomZap
} }
} }
-28
View File
@@ -1,28 +0,0 @@
//
// ZapUserView.swift
// damus
//
// Created by William Casarin on 2023-06-22.
//
import SwiftUI
struct ZapUserView: View {
let state: DamusState
let pubkey: String
var body: some View {
HStack(alignment: .center) {
Text("Zap")
.font(.title2)
UserView(damus_state: state, pubkey: pubkey, spacer: false)
}
}
}
struct ZapUserView_Previews: PreviewProvider {
static var previews: some View {
ZapUserView(state: test_damus_state(), pubkey: ANON_PUBKEY)
}
}

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