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