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,32 +94,9 @@ 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
+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)")
+96 -144
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)
+73 -116
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 { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return return
} }
if !self.notifications.insert_zap(.zap(zap)) { damus_state.add_zap(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 { guard zap.target.pubkey == our_keypair.pubkey else {
return return
} }
if self.damus_state.settings.zap_vibration { if !notifications.insert_zap(.zap(zap)) {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
if 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)
} }
} }
}
self.notification_status.new_events = new_bits 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
}
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
@@ -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 {
+7 -6
View File
@@ -36,31 +36,32 @@ 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: {
if state.keypair.privkey == nil { if state.keypair.privkey == nil {
+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)
+5 -2
View File
@@ -21,6 +21,10 @@ struct DirectMessagesView: View {
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
+34 -67
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)
} }
@@ -193,8 +161,8 @@ struct EventGroupView: View {
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)
@@ -11,12 +11,19 @@ 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: " ")
@@ -131,14 +112,6 @@ struct PostView: View {
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: {
attach_media = true attach_media = true
@@ -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)
} }
+114 -105
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)
@@ -118,15 +93,18 @@ 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
@@ -269,7 +247,7 @@ struct ProfileView: View {
func lnButton(lnurl: String, profile: Profile) -> some View { func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap" let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: { return Button(action: {
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)
@@ -296,11 +274,44 @@ 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)
} }
@@ -324,7 +335,7 @@ 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)
} }
} }
@@ -392,7 +403,28 @@ struct ProfileView: View {
nameSection(profile_data: profile_data) nameSection(profile_data: profile_data)
if let about = profile_data?.about { if let about = profile_data?.about {
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 {
@@ -403,7 +435,7 @@ struct ProfileView: View {
if let contact = profile.contacts { if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id } let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(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())
@@ -431,102 +464,78 @@ struct ProfileView: View {
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() { VStack() {
aboutSection aboutSection
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 your notes (instead of notes and replies).").tag(FilterState.posts) Text("Posts", comment: "Label for filter for seeing only your posts (instead of posts 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) Text("Posts & Replies", comment: "Label for filter for seeing your posts and replies (instead of only your posts).").tag(FilterState.posts_and_replies)
}) })
Divider() Divider()
.frame(height: 1) .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) .background(colorScheme == .dark ? Color.black : Color.white)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
} if filter_state == FilterState.posts {
} InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
.ignoresSafeArea() }
.navigationTitle("") if filter_state == FilterState.posts_and_replies {
.navigationBarHidden(true) InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
.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)
}
} }
} }
+68 -239
View File
@@ -8,54 +8,12 @@
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 {
return nil return nil
@@ -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 { VStack(alignment: .center) {
Text(display_name)
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.font(.body)
}
Spacer() let profile = damus_state.profiles.lookup(id: pubkey)
if let key = maybe_key { if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key)) ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.interpolation(.none) .padding(.top, 50)
.resizable() } else {
.scaledToFit() Image(systemName: "person.fill")
.frame(width: 300, height: 300) .font(.system(size: 60))
.cornerRadius(10) .foregroundColor(DamusColors.white)
.overlay(RoundedRectangle(cornerRadius: 10) .padding(.top, 50)
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.font(.system(size: 18, weight: .ultraLight))
Spacer()
Button(action: {
selectedTab = 1
}) {
HStack {
Text("Scan Code", comment: "Button to switch to scan QR Code page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(50)
}
}
func QRCameraView() -> some View {
return VStack(alignment: .center) {
Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.")
.padding(.top, 50)
.font(.system(size: 24, weight: .heavy))
Spacer()
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in
switch result {
case .success(let success):
handleProfileScan(success.string)
case .failure(let failure):
self.error = failure.localizedDescription
}
}
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
.rotationEffect(.degrees(-90)))
.shadow(radius: 10)
Spacer()
Spacer()
Button(action: {
selectedTab = 0
}) {
HStack {
Text("View QR Code", comment: "Button to switch to view users QR Code")
.fontWeight(.semibold)
}
.frame( maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(50)
}
}
func handleProfileScan(_ scanned_str: String) {
guard let result = ProfileScanResult(string: scanned_str) else {
self.error = "Invalid profile QR"
return
}
self.error = nil
guard result != self.scanResult else {
return
}
generator.impactOccurred()
cameraAnimate {
scanResult = result
find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in
guard let res else {
error = "Profile not found"
return
} }
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()
}
}
func show_profile_after_delay() { if let key = maybe_key {
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
if let scanResult { .interpolation(.none)
damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey)) .resizable()
} .scaledToFit()
} .frame(width: 200, height: 200)
} .padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
if (pubkey == damus_state.pubkey) {
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
} else {
Text("Follow them on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user (someone else).")
.foregroundColor(DamusColors.white)
.font(.system(size: 24, weight: .heavy))
.padding(.top)
}
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.foregroundColor(DamusColors.white)
.font(.system(size: 18, weight: .ultraLight))
Spacer()
}
func cameraAnimate(completion: @escaping () -> Void) {
outerTrimEnd = 0.0
withAnimation(.easeInOut(duration: animationDuration)) {
outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done.
} }
completion() .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
} }
func generateQRCode(pubkey: String) -> UIImage { 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: "")
} }
} }
+23 -17
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")
@@ -31,12 +35,17 @@ struct SetupView: View {
.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")
.font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
}
Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is") 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)
} }
} }
+10 -8
View File
@@ -11,6 +11,7 @@ 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
@@ -44,11 +45,11 @@ struct SideMenuView: View {
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 {
@@ -63,19 +64,19 @@ struct SideMenuView: View {
}*/ }*/
} }
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")
} }
} }
@@ -87,7 +88,8 @@ struct SideMenuView: View {
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)
@@ -107,7 +109,7 @@ struct SideMenuView: View {
} }
} }
.padding(.bottom, verticalSpacing) .padding(.bottom, verticalSpacing)
}) }
Divider() Divider()
+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? {
+1 -11
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,
+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) { if model.has_audio == true {
VideoPlayer(url: url, model: model) MuteIcon
if model.has_audio == true { .zIndex(11.0)
MuteIcon .onTapGesture {
.zIndex(11.0) self.model.muted = !self.model.muted
.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 }
+6 -3
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
@@ -65,12 +64,16 @@ 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)
+4 -6
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