Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9e49e8c2f1
|
|||
| dde48132c9 | |||
| 809a08ef63 | |||
| 57e6f083b8 | |||
| cfa1e13887 | |||
| 6eecb5ef26 | |||
| 5dc3e2635e | |||
| 2713e76e17 | |||
| 6aa28fce6c | |||
| a6fb175b98 | |||
| 554c091d57 | |||
| 9e359650bf | |||
| bb091d072f | |||
| 88b04fde09 | |||
| a1753b2c24 | |||
| 80fac1903e | |||
| 6214ab8d8f | |||
| 85cd1bea19 | |||
| 4d95d36a1e | |||
| 185fba150f | |||
| 06ba0f7387 | |||
| 51c4fa1e32 | |||
| ed4ef0e215 | |||
| 973e9fe714 | |||
| d12281fcc5 | |||
| 156d885e6e | |||
| bf01d08ea0 | |||
| 548d8e49ec | |||
| 0fc38e5c05 | |||
| 028cab9cf6 | |||
| 29068a40c8 | |||
| ff06cfaf0c | |||
| eb72fb481c | |||
| 5b7339a0de | |||
| 06ba72a23d | |||
| 47d594af00 | |||
| dd022a953c | |||
| a4cbf6a12a | |||
| 62dfc24e58 | |||
| cf15bd3463 | |||
| fd44a56f9b | |||
| 9a9fb28a48 | |||
| ba7f675300 | |||
| 8e84b446a5 | |||
| 8dbe2c728f | |||
| f975723c0f | |||
| 8ed04f0b0e | |||
| e33cb3b1c3 | |||
| f171cfffe7 | |||
| 46556c8bdc | |||
| 10fec3cbf8 | |||
| 7250cf0d8f | |||
| 2bddd5ce92 | |||
| 027b39caed | |||
| 122d0e451e | |||
| f99e311e58 | |||
| 3fbf3cc12e | |||
| 6682f792d8 | |||
| 2e53ce3905 | |||
| c238b6e28f | |||
| e99142a5b8 | |||
| 5716578c20 | |||
| ec35a413fe | |||
| 724773cb45 | |||
| 15acdef912 | |||
| 72cc4c1fd7 | |||
| 65f4227e9b | |||
| f0be5a5ad8 | |||
| 48855d5d41 | |||
| 90c22fdabd | |||
| dfd1032cd8 | |||
| a0e1e16f17 | |||
| 020a00bf7e | |||
| 297aaf86c6 | |||
| f6dd060580 | |||
| 12428d01ad | |||
| d091543448 | |||
| 2e596a47a1 | |||
| ff15156297 | |||
| 0a9fbf5031 | |||
| 8eebc2abe5 | |||
| c22199165a | |||
| f7a0370824 | |||
| fe4277e817 | |||
| 6804fbb607 | |||
| 05503024cc | |||
| e4e477a2ac | |||
| 59e7a42b5f | |||
| 0552c24108 | |||
| 61303f49ad | |||
| e10dc93233 | |||
| ea73c5252d | |||
| 7e963c9025 | |||
| 61ff7da2ae | |||
| 7259641e26 | |||
| 91113fbc6d | |||
| d58a1e0ba3 | |||
| 3f7b0a4d6e | |||
| bc315dd571 | |||
| 32431096f5 | |||
| 6172347455 | |||
| 31d327a085 | |||
| b5ae7df795 | |||
| 3b0bb48dd4 | |||
| 4646f0e23c | |||
| 7027b7016c | |||
| 76c57af548 | |||
| 7983157c38 |
@@ -1,3 +1,35 @@
|
||||
## [1.5-2] - 2023-05-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add new full-bleed video player (William Casarin)
|
||||
- Add ability to show multiple posts per user in Universe (Ben Weeks)
|
||||
- Custom iconography added for other areas of the app. (Ben Weeks)
|
||||
- Custom iconography for the left navigation. (Ben Weeks)
|
||||
- Custom iconography for the tab buttons. (Ben Weeks)
|
||||
- Added dots under image carousel (Ben Weeks)
|
||||
- Add profile caching (Bryan Montz)
|
||||
- Add mention parsing and fine-grained text selection on description in ProfileView (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesign phase 1 (text, icons)
|
||||
- Updated UI to use custom font (Ben Weeks)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix side menu bug in landscape (OlegAba)
|
||||
- Use "Follow me on nostr" text when looking at someone else's QR code (Ben Weeks)
|
||||
- Fix issue where cursor dissapears when typing long message (gladiusKatana)
|
||||
- Attempt fix for randomly broken animated gifs (William Casarin)
|
||||
- Fix cursor jumping when pressing return (gladius)
|
||||
- Fix side menu label size so that translations in longer languages fit without wrapping (Terry Yiu)
|
||||
- Fix reaction notification title to be consistent with ReactionView (Terry Yiu)
|
||||
- Fix nostr URL scheme to open properly even if there's already a different view open (Terry Yiu)
|
||||
- Fix crash related to preloading events (Bryan Montz)
|
||||
|
||||
|
||||
## v1.4.3 - 2023-05-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
@@ -49,6 +50,7 @@
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
|
||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */; };
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@@ -207,6 +209,8 @@
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */; };
|
||||
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
@@ -267,12 +271,19 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
||||
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.swift */; };
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
|
||||
@@ -288,6 +299,7 @@
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.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 */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; };
|
||||
@@ -326,6 +338,7 @@
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonModel.swift; sourceTree = "<group>"; };
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@@ -443,6 +456,7 @@
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayer.swift; sourceTree = "<group>"; };
|
||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
@@ -635,6 +649,7 @@
|
||||
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
@@ -699,12 +714,19 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
|
||||
6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.swift; sourceTree = "<group>"; };
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
|
||||
@@ -720,6 +742,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||
@@ -739,6 +762,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -881,6 +905,7 @@
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
|
||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -912,10 +937,20 @@
|
||||
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
|
||||
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
|
||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C1A9A2829DDF53B00516EAC /* Video */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
|
||||
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */,
|
||||
);
|
||||
path = Video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -942,6 +977,7 @@
|
||||
4C7D09692A0AEA0400943473 /* CodeScanner */,
|
||||
4C7D095A2A098C5C00943473 /* Wallet */,
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
||||
4C1A9A2829DDF53B00516EAC /* Video */,
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
@@ -1006,6 +1042,7 @@
|
||||
4C75EFAB28049CC80006080F /* Nostr */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
501F8C5329FF5EE2001AFC1D /* CoreData */,
|
||||
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
|
||||
4C75EFA527FF87A20006080F /* Nostr.swift */,
|
||||
4C75EFAE28049D340006080F /* NostrFilter.swift */,
|
||||
@@ -1016,6 +1053,7 @@
|
||||
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
|
||||
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
|
||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
|
||||
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
|
||||
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
|
||||
4C363A8F28247A1D006E126D /* NostrLink.swift */,
|
||||
50088DA029E8271A008A1FDF /* WebSocket.swift */,
|
||||
@@ -1049,6 +1087,8 @@
|
||||
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
|
||||
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
|
||||
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
|
||||
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */,
|
||||
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
|
||||
);
|
||||
path = Gradients;
|
||||
sourceTree = "<group>";
|
||||
@@ -1227,6 +1267,7 @@
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
||||
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
|
||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -1302,6 +1343,7 @@
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
|
||||
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */,
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
|
||||
@@ -1393,6 +1435,15 @@
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
501F8C5329FF5EE2001AFC1D /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */,
|
||||
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C0F392D29B57C8F0039859C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1437,6 +1488,7 @@
|
||||
packageProductDependencies = (
|
||||
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -1541,6 +1593,7 @@
|
||||
packageReferences = (
|
||||
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1598,6 +1651,7 @@
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */,
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||
@@ -1674,6 +1728,7 @@
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
|
||||
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
|
||||
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
|
||||
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||
@@ -1738,9 +1793,11 @@
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
@@ -1749,6 +1806,7 @@
|
||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */,
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
||||
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
||||
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */,
|
||||
@@ -1764,12 +1822,15 @@
|
||||
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
|
||||
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
|
||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
||||
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */,
|
||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
||||
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
|
||||
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
||||
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
@@ -1823,6 +1884,7 @@
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
|
||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
|
||||
@@ -1841,6 +1903,7 @@
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
|
||||
@@ -1856,6 +1919,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
@@ -2140,7 +2204,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2188,7 +2252,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2361,6 +2425,14 @@
|
||||
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
|
||||
};
|
||||
};
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/wxxsw/GSPlayer";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.2.26;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -2374,7 +2446,25 @@
|
||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||
productName = secp256k1;
|
||||
};
|
||||
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */;
|
||||
productName = GSPlayer;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */,
|
||||
);
|
||||
currentVersion = 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */;
|
||||
path = Damus.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
/* End XCVersionGroup section */
|
||||
};
|
||||
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "gsplayer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wxxsw/GSPlayer",
|
||||
"state" : {
|
||||
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
|
||||
"version" : "0.2.26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shaka-full.pdf",
|
||||
"filename" : "eula-bg.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<svg width="430" height="813" viewBox="0 0 430 813" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_1069_29012)">
|
||||
<path d="M44.0811 256.851L186.315 111L276.203 223.574L244.02 388.295L69.9751 697.084L100.678 498.338L44.0811 256.851Z" fill="url(#paint0_linear_1069_29012)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_1069_29012)">
|
||||
<path d="M116.509 587.348L206.677 479.401L230.746 273.265L266.666 231.183L367.424 396.975L281.292 659.008L266.665 801.413L66.889 763.694L116.509 587.348Z" fill="url(#paint1_linear_1069_29012)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1069_29012" x="-66.6248" y="0.294121" width="453.534" height="807.496" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1069_29012" x="-43.8172" y="120.477" width="521.947" height="791.642" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1069_29012" x1="230.179" y1="166.577" x2="-67.7956" y2="310.108" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D34CD9"/>
|
||||
<stop offset="1" stop-color="#4E4DF4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1069_29012" x1="139.483" y1="462.902" x2="377.854" y2="565.47" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0DE8FF"/>
|
||||
<stop offset="1" stop-color="#641AAE"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,15 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shaka-line.pdf",
|
||||
"filename" : "lightbulb.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.2" width="48" height="48" rx="24" fill="url(#paint0_linear_1843_42349)"/>
|
||||
<path d="M19.9993 36.0001H27.9993M28.8095 28.0001C31.5199 26.3669 33.3327 23.3952 33.3327 20.0001C33.3327 14.8454 29.154 10.6667 23.9993 10.6667C18.8447 10.6667 14.666 14.8454 14.666 20.0001C14.666 23.3952 16.4788 26.3669 19.1892 28.0001M28.8095 28.0001C28.5475 28.1579 28.2772 28.3032 27.9993 28.4352V31.3334C27.9993 31.7016 27.7009 32.0001 27.3327 32.0001H20.666C20.2978 32.0001 19.9993 31.7016 19.9993 31.3334V28.4352C19.7215 28.3032 19.4512 28.1579 19.1892 28.0001M28.8095 28.0001H19.1892" stroke="url(#paint1_linear_1843_42349)" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1843_42349" x1="5.41935" y1="0.774194" x2="37.9355" y2="47.2258" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9AD1C"/>
|
||||
<stop offset="1" stop-color="#DF7E0C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1843_42349" x1="16.7735" y1="11.0754" x2="35.0141" y2="30.2759" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9AD1C"/>
|
||||
<stop offset="1" stop-color="#DF7E0C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "header.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nostr-logo.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -1,88 +0,0 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.073975 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
1.295334 8.661732 m
|
||||
3.613694 8.367855 l
|
||||
4.475733 8.733568 5.268113 9.771931 5.474915 10.327032 c
|
||||
6.083156 11.959681 5.507567 14.604573 5.474915 15.061715 c
|
||||
5.448792 15.427428 6.008246 15.693006 6.291239 15.780080 c
|
||||
7.571236 15.858447 8.508359 14.876789 8.642253 13.984165 c
|
||||
8.740212 13.331103 8.576948 11.752880 8.381030 10.849482 c
|
||||
8.979668 10.936556 10.980525 10.901726 11.868687 10.849482 c
|
||||
12.756847 10.797236 13.474895 10.196423 14.193260 9.412750 c
|
||||
14.767952 8.237244 13.953805 7.725680 13.474895 7.616838 c
|
||||
13.834077 7.257654 l
|
||||
14.781013 5.918882 13.649043 5.178749 13.115711 5.004600 c
|
||||
13.474895 4.743376 l
|
||||
14.487136 3.763786 13.246323 2.751544 13.017752 2.882155 c
|
||||
11.058574 3.176033 l
|
||||
15.499378 1.673996 l
|
||||
16.054478 0.400530 15.074889 0.073999 14.781013 0.073999 c
|
||||
8.576947 1.673996 l
|
||||
6.291239 1.673996 5.311650 1.869914 4.299407 2.163791 c
|
||||
4.157911 2.131138 3.659409 1.987464 2.797370 1.673996 c
|
||||
1.935332 1.360527 1.219143 2.087601 0.968804 2.490320 c
|
||||
-0.285071 4.083785 -0.467927 7.257655 1.295334 8.661732 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1149
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 15.666626 15.710510 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001239 00000 n
|
||||
0000001262 00000 n
|
||||
0000001435 00000 n
|
||||
0000001509 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1568
|
||||
%%EOF
|
||||
@@ -1,323 +0,0 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.474731 -0.563965 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
3.613694 9.332577 m
|
||||
3.553993 8.861599 l
|
||||
3.637261 8.851044 3.721838 8.862753 3.799107 8.895533 c
|
||||
3.613694 9.332577 l
|
||||
h
|
||||
1.295334 9.626453 m
|
||||
1.355035 10.097433 l
|
||||
1.227973 10.113539 1.099794 10.077623 0.999601 9.997839 c
|
||||
1.295334 9.626453 l
|
||||
h
|
||||
0.968804 3.455042 m
|
||||
1.372000 3.705677 l
|
||||
1.362764 3.720535 1.352713 3.734872 1.341894 3.748621 c
|
||||
0.968804 3.455042 l
|
||||
h
|
||||
4.299407 3.128512 m
|
||||
4.431771 3.584435 l
|
||||
4.353942 3.607030 4.271623 3.609325 4.192656 3.591103 c
|
||||
4.299407 3.128512 l
|
||||
h
|
||||
8.576947 2.638718 m
|
||||
8.695503 3.098424 l
|
||||
8.656776 3.108411 8.616942 3.113465 8.576947 3.113465 c
|
||||
8.576947 2.638718 l
|
||||
h
|
||||
14.781013 1.038721 m
|
||||
14.662457 0.579016 l
|
||||
14.701184 0.569027 14.741018 0.563974 14.781013 0.563974 c
|
||||
14.781013 1.038721 l
|
||||
h
|
||||
15.499378 2.638718 m
|
||||
15.934578 2.828420 l
|
||||
15.881091 2.951125 15.778289 3.045548 15.651489 3.088437 c
|
||||
15.499378 2.638718 l
|
||||
h
|
||||
11.058574 4.140755 m
|
||||
11.128998 4.610250 l
|
||||
10.885809 4.646729 10.655017 4.491467 10.597156 4.252461 c
|
||||
10.539293 4.013455 10.673516 3.769826 10.906463 3.691035 c
|
||||
11.058574 4.140755 l
|
||||
h
|
||||
13.017752 3.846877 m
|
||||
13.253292 4.259073 l
|
||||
13.202273 4.288227 13.146286 4.307655 13.088176 4.316372 c
|
||||
13.017752 3.846877 l
|
||||
h
|
||||
13.474895 5.708097 m
|
||||
13.805044 6.049252 l
|
||||
13.789093 6.064689 13.772079 6.078987 13.754128 6.092043 c
|
||||
13.474895 5.708097 l
|
||||
h
|
||||
13.115711 5.969321 m
|
||||
12.968349 6.420619 l
|
||||
12.798800 6.365256 12.674588 6.219535 12.646772 6.043359 c
|
||||
12.618958 5.867183 12.692234 5.690281 12.836478 5.585376 c
|
||||
13.115711 5.969321 l
|
||||
h
|
||||
13.834077 8.222376 m
|
||||
14.221668 8.496526 l
|
||||
14.206144 8.518474 14.188784 8.539063 14.169774 8.558073 c
|
||||
13.834077 8.222376 l
|
||||
h
|
||||
13.474895 8.581559 m
|
||||
13.369680 9.044500 l
|
||||
13.201114 9.006190 13.066693 8.879284 13.018762 8.713197 c
|
||||
12.970830 8.547110 13.016963 8.368095 13.139197 8.245862 c
|
||||
13.474895 8.581559 l
|
||||
h
|
||||
14.193260 10.377472 m
|
||||
14.619765 10.585986 l
|
||||
14.599768 10.626891 14.573989 10.664707 14.543221 10.698271 c
|
||||
14.193260 10.377472 l
|
||||
h
|
||||
8.381030 11.814203 m
|
||||
7.917068 11.914822 l
|
||||
7.884080 11.762714 7.927746 11.604099 8.033934 11.490305 c
|
||||
8.140121 11.376513 8.295343 11.321997 8.449365 11.344399 c
|
||||
8.381030 11.814203 l
|
||||
h
|
||||
8.642253 14.948887 m
|
||||
9.111748 15.019311 l
|
||||
8.642253 14.948887 l
|
||||
h
|
||||
6.291239 16.744801 m
|
||||
6.262227 17.218662 l
|
||||
6.224693 17.216364 6.187564 17.209614 6.151623 17.198555 c
|
||||
6.291239 16.744801 l
|
||||
h
|
||||
5.474915 16.026436 m
|
||||
5.948456 16.060261 l
|
||||
5.474915 16.026436 l
|
||||
h
|
||||
5.474915 11.291754 m
|
||||
5.030037 11.457493 l
|
||||
5.474915 11.291754 l
|
||||
h
|
||||
3.673396 9.803555 m
|
||||
1.355035 10.097433 l
|
||||
1.235632 9.155476 l
|
||||
3.553993 8.861599 l
|
||||
3.673396 9.803555 l
|
||||
h
|
||||
0.999601 9.997839 m
|
||||
-0.029049 9.178730 -0.454726 7.875908 -0.474048 6.619066 c
|
||||
-0.493367 5.362488 -0.110331 4.058727 0.595713 3.161463 c
|
||||
1.341894 3.748621 l
|
||||
0.794064 4.444821 0.458734 5.524729 0.475334 6.604470 c
|
||||
0.491930 7.683949 0.856455 8.670100 1.591066 9.255068 c
|
||||
0.999601 9.997839 l
|
||||
h
|
||||
0.565608 3.204407 m
|
||||
0.721970 2.952868 1.013515 2.611341 1.407507 2.372385 c
|
||||
1.811404 2.127421 2.357187 1.973489 2.959612 2.192553 c
|
||||
2.635129 3.084882 l
|
||||
2.375515 2.990478 2.132184 3.043347 1.899893 3.184233 c
|
||||
1.657696 3.331126 1.465977 3.554496 1.372000 3.705677 c
|
||||
0.565608 3.204407 l
|
||||
h
|
||||
2.959612 2.192553 m
|
||||
3.816493 2.504146 4.293336 2.639887 4.406158 2.665923 c
|
||||
4.192656 3.591103 l
|
||||
4.022485 3.551832 3.502325 3.400227 2.635129 3.084882 c
|
||||
2.959612 2.192553 l
|
||||
h
|
||||
4.167043 2.672591 m
|
||||
5.229115 2.364247 6.254152 2.163970 8.576947 2.163970 c
|
||||
8.576947 3.113465 l
|
||||
6.328326 3.113465 5.394184 3.305025 4.431771 3.584435 c
|
||||
4.167043 2.672591 l
|
||||
h
|
||||
8.458392 2.179011 m
|
||||
14.662457 0.579016 l
|
||||
14.899569 1.498427 l
|
||||
8.695503 3.098424 l
|
||||
8.458392 2.179011 l
|
||||
h
|
||||
14.781013 0.563974 m
|
||||
15.036198 0.563974 15.495326 0.684875 15.814721 1.047266 c
|
||||
16.180891 1.462728 16.264221 2.072176 15.934578 2.828420 c
|
||||
15.064179 2.449016 l
|
||||
15.289635 1.931793 15.160722 1.741243 15.102402 1.675073 c
|
||||
15.055794 1.622190 14.990156 1.579316 14.916806 1.549556 c
|
||||
14.881134 1.535082 14.847747 1.525430 14.820526 1.519657 c
|
||||
14.791491 1.513498 14.777695 1.513469 14.781013 1.513469 c
|
||||
14.781013 0.563974 l
|
||||
h
|
||||
15.651489 3.088437 m
|
||||
11.210685 4.590474 l
|
||||
10.906463 3.691035 l
|
||||
15.347267 2.188998 l
|
||||
15.651489 3.088437 l
|
||||
h
|
||||
10.988150 3.671260 m
|
||||
12.947328 3.377382 l
|
||||
13.088176 4.316372 l
|
||||
11.128998 4.610250 l
|
||||
10.988150 3.671260 l
|
||||
h
|
||||
12.782211 3.434681 m
|
||||
12.991495 3.315090 13.204453 3.370091 13.288217 3.396689 c
|
||||
13.400116 3.432221 13.506123 3.490767 13.598186 3.554502 c
|
||||
13.783985 3.683133 13.977411 3.877748 14.120350 4.119644 c
|
||||
14.264680 4.363894 14.369576 4.678114 14.335162 5.031647 c
|
||||
14.300108 5.391746 14.125634 5.739002 13.805044 6.049252 c
|
||||
13.144745 5.366943 l
|
||||
13.330275 5.187398 13.380290 5.040778 13.390134 4.939653 c
|
||||
13.400617 4.831963 13.370820 4.717613 13.302905 4.602680 c
|
||||
13.233600 4.485394 13.137231 4.390213 13.057724 4.335170 c
|
||||
13.017135 4.307070 12.996612 4.300308 13.000857 4.301657 c
|
||||
13.003194 4.302399 13.024761 4.309311 13.061064 4.310122 c
|
||||
13.095938 4.310902 13.170414 4.306433 13.253292 4.259073 c
|
||||
12.782211 3.434681 l
|
||||
h
|
||||
13.754128 6.092043 m
|
||||
13.394944 6.353267 l
|
||||
12.836478 5.585376 l
|
||||
13.195662 5.324152 l
|
||||
13.754128 6.092043 l
|
||||
h
|
||||
13.263074 5.518023 m
|
||||
13.593105 5.625790 14.123367 5.907292 14.433812 6.409482 c
|
||||
14.595931 6.671733 14.696482 6.993351 14.669847 7.364054 c
|
||||
14.643518 7.730516 14.495621 8.109214 14.221668 8.496526 c
|
||||
13.446486 7.948226 l
|
||||
13.646002 7.666152 13.711709 7.450294 13.722795 7.296009 c
|
||||
13.733575 7.145966 13.695351 7.020646 13.626177 6.908748 c
|
||||
13.474038 6.662641 13.171650 6.487002 12.968349 6.420619 c
|
||||
13.263074 5.518023 l
|
||||
h
|
||||
14.169774 8.558073 m
|
||||
13.810592 8.917255 l
|
||||
13.139197 8.245862 l
|
||||
13.498380 7.886679 l
|
||||
14.169774 8.558073 l
|
||||
h
|
||||
13.580109 8.118617 m
|
||||
13.896242 8.190466 14.344993 8.395787 14.624650 8.816864 c
|
||||
14.929440 9.275781 14.963785 9.882310 14.619765 10.585986 c
|
||||
13.766754 10.168959 l
|
||||
13.997427 9.697128 13.912044 9.460121 13.833706 9.342171 c
|
||||
13.730235 9.186377 13.532457 9.081495 13.369680 9.044500 c
|
||||
13.580109 8.118617 l
|
||||
h
|
||||
14.543221 10.698271 m
|
||||
13.820906 11.486253 12.989320 12.223852 11.896564 12.288132 c
|
||||
11.840808 11.340275 l
|
||||
12.524374 11.300065 13.128883 10.836036 13.843298 10.056674 c
|
||||
14.543221 10.698271 l
|
||||
h
|
||||
11.896564 12.288132 m
|
||||
11.441970 12.314873 10.711069 12.336796 10.019300 12.341186 c
|
||||
9.341933 12.345484 8.654247 12.333687 8.312695 12.284006 c
|
||||
8.449365 11.344399 l
|
||||
8.706450 11.381794 9.318512 11.396118 10.013274 11.391710 c
|
||||
10.693633 11.387392 11.407242 11.365778 11.840808 11.340275 c
|
||||
11.896564 12.288132 l
|
||||
h
|
||||
8.844993 11.713585 m
|
||||
8.948084 12.188952 9.040332 12.829445 9.094679 13.432834 c
|
||||
9.147870 14.023395 9.169946 14.631327 9.111748 15.019311 c
|
||||
8.172758 14.878462 l
|
||||
8.212520 14.613384 8.201942 14.105675 8.149012 13.518009 c
|
||||
8.097237 12.943172 8.009893 12.342852 7.917068 11.914822 c
|
||||
8.844993 11.713585 l
|
||||
h
|
||||
9.111748 15.019311 m
|
||||
8.944062 16.137217 7.805658 17.313158 6.262227 17.218662 c
|
||||
6.320251 16.270941 l
|
||||
7.336813 16.333179 8.072657 15.545805 8.172758 14.878462 c
|
||||
9.111748 15.019311 l
|
||||
h
|
||||
6.151623 17.198555 m
|
||||
5.976391 17.144638 5.715709 17.036982 5.490986 16.876261 c
|
||||
5.292936 16.734617 4.969444 16.439627 5.001374 15.992612 c
|
||||
5.948456 16.060261 l
|
||||
5.951383 16.019283 5.934667 15.999795 5.943361 16.012491 c
|
||||
5.954769 16.029152 5.984430 16.061831 6.043331 16.103956 c
|
||||
6.162553 16.189222 6.323094 16.257891 6.430855 16.291048 c
|
||||
6.151623 17.198555 l
|
||||
h
|
||||
5.001374 15.992612 m
|
||||
5.011176 15.855374 5.059216 15.566318 5.104405 15.255149 c
|
||||
5.152757 14.922197 5.207128 14.509316 5.241940 14.062993 c
|
||||
5.312967 13.152368 5.295928 12.171200 5.030037 11.457493 c
|
||||
5.919792 11.126015 l
|
||||
6.262142 12.044956 6.261431 13.202559 6.188560 14.136827 c
|
||||
6.151423 14.612950 6.093790 15.049047 6.044043 15.391605 c
|
||||
5.991133 15.755945 5.954979 15.968927 5.948456 16.060261 c
|
||||
5.001374 15.992612 l
|
||||
h
|
||||
5.030037 11.457493 m
|
||||
4.953650 11.252455 4.742510 10.903708 4.434547 10.555828 c
|
||||
4.127778 10.209298 3.769400 9.914337 3.428282 9.769621 c
|
||||
3.799107 8.895533 l
|
||||
4.320028 9.116529 4.788858 9.523607 5.145489 9.926461 c
|
||||
5.500926 10.327968 5.789377 10.775953 5.919792 11.126015 c
|
||||
5.030037 11.457493 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
7995
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 16.615845 16.660034 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000008085 00000 n
|
||||
0000008108 00000 n
|
||||
0000008281 00000 n
|
||||
0000008355 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
8414
|
||||
%%EOF
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// GradientButtonStyle.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GradientButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
return configuration.label
|
||||
.padding()
|
||||
.foregroundColor(Color.white)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(PinkGradient.gradient)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? 0.8 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct GradientButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Button("Dynamic Size", action: {
|
||||
print("dynamic size")
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
|
||||
|
||||
Button(action: {
|
||||
print("infinite width")
|
||||
}) {
|
||||
HStack {
|
||||
Text("Infinite Width")
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// DamusLogoGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/24/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let damus_logo_grad_c1 = hex_col(r: 0x30, g: 0xb3, b: 0xf1)
|
||||
fileprivate let damus_logo_grad_c2 = hex_col(r: 0xc5, g: 0x39, b: 0xf9)
|
||||
fileprivate let damus_logo_grad = [damus_logo_grad_c1, damus_logo_grad_c2]
|
||||
|
||||
struct DamusLogoGradient: View {
|
||||
var body: some View {
|
||||
DamusLogoGradient.gradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: damus_logo_grad, startPoint: .leading, endPoint: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusLogoGradient_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusLogoGradient()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// PinkGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x4c, b: 0xd9)
|
||||
fileprivate let damus_grad_c2 = hex_col(r: 0xf8, g: 0x69, b: 0xb6)
|
||||
fileprivate let pink_grad = [damus_grad_c1, damus_grad_c2]
|
||||
|
||||
struct PinkGradient: View {
|
||||
var body: some View {
|
||||
PinkGradient.gradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
struct PinkGradient_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PinkGradient()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct IconLabel: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: img_name)
|
||||
Image(img_name)
|
||||
.foregroundColor(img_color)
|
||||
.frame(width: 20)
|
||||
.padding([.trailing], 20)
|
||||
|
||||
@@ -52,8 +52,9 @@ enum ImageShape {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Carousel
|
||||
struct ImageCarousel: View {
|
||||
var urls: [URL]
|
||||
var urls: [MediaUrl]
|
||||
|
||||
let evid: String
|
||||
|
||||
@@ -62,14 +63,19 @@ struct ImageCarousel: View {
|
||||
@State private var open_sheet: Bool = false
|
||||
@State private var current_url: URL? = nil
|
||||
@State private var image_fill: ImageFill? = nil
|
||||
|
||||
@State private var fillHeight: CGFloat = 350
|
||||
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
@State private var firstImageHeight: CGFloat? = nil
|
||||
@State private var currentImageHeight: CGFloat?
|
||||
@State private var selectedIndex = 0
|
||||
@State private var video_size: CGSize? = nil
|
||||
|
||||
let fillHeight: CGFloat = 350
|
||||
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2
|
||||
|
||||
init(state: DamusState, evid: String, urls: [URL]) {
|
||||
init(state: DamusState, evid: String, urls: [MediaUrl]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
_image_fill = State(initialValue: media_model.fill)
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
@@ -80,75 +86,154 @@ struct ImageCarousel: View {
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
image_fill?.height ?? fillHeight
|
||||
firstImageHeight ?? image_fill?.height ?? fillHeight
|
||||
}
|
||||
|
||||
func Placeholder(url: URL, geo_size: CGSize) -> some View {
|
||||
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
||||
Group {
|
||||
if let meta = state.events.lookup_img_metadata(url: url),
|
||||
if num_urls > 1 {
|
||||
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
|
||||
Color.clear
|
||||
} else if let meta = state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
|
||||
} else {
|
||||
EmptyView()
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil,
|
||||
let meta = state.events.lookup_img_metadata(url: url),
|
||||
let size = meta.meta.dim?.size
|
||||
{
|
||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(urls, id: \.absoluteString) { url in
|
||||
func video_model(_ url: URL) -> VideoPlayerModel {
|
||||
return state.events.get_video_player_model(url: url)
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
Group {
|
||||
switch url {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
||||
.onChange(of: video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
|
||||
}
|
||||
|
||||
var Medias: some View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
GeometryReader { geo in
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
.frame(height: self.height)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
.frame(height: height)
|
||||
.onChange(of: selectedIndex) { value in
|
||||
selectedIndex = value
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Medias
|
||||
.onTapGesture { }
|
||||
|
||||
// This is our custom carousel image indicator
|
||||
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Carousel
|
||||
struct CarouselDotsView<T>: View {
|
||||
let urls: [T]
|
||||
@Binding var selectedIndex: Int
|
||||
|
||||
var body: some View {
|
||||
if urls.count > 1 {
|
||||
HStack {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
|
||||
.frame(width: 10, height: 10)
|
||||
.onTapGesture {
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, CGFloat(8))
|
||||
.id(UUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Modifier
|
||||
@@ -199,9 +284,11 @@ public struct ImageFill {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Provider
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ struct InvoiceView: View {
|
||||
UIPasteboard.general.string = invoice.string
|
||||
} label: {
|
||||
if !copied {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
Image("copy2")
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Image(systemName: "checkmark.circle")
|
||||
Image("check-circle")
|
||||
.foregroundColor(DamusColors.green)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ struct InvoiceView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("", systemImage: "bolt.fill")
|
||||
Label("", image: "zap.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||
Spacer()
|
||||
|
||||
@@ -32,11 +32,11 @@ struct NIP05Badge: View {
|
||||
Group {
|
||||
if nip05_color {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image(systemName: "checkmark.seal.fill")
|
||||
.mask(Image("check-circle.fill")
|
||||
.resizable()
|
||||
).frame(width: 14, height: 14)
|
||||
} else if show_domain {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
Image("check-circle.fill")
|
||||
.font(.footnote)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct Reposted: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
Image("repost")
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
|
||||
@@ -43,7 +43,9 @@ struct UserView: View {
|
||||
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 {
|
||||
Text(about)
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
Text(about_string)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ struct WebsiteLink: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "link")
|
||||
Image("link")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
|
||||
|
||||
@@ -10,29 +10,25 @@ import SwiftUI
|
||||
enum ZappingEventType {
|
||||
case failed(ZappingError)
|
||||
case got_zap_invoice(String)
|
||||
case sent_from_nwc
|
||||
}
|
||||
|
||||
enum ZappingError {
|
||||
case fetching_invoice
|
||||
case bad_lnurl
|
||||
case canceled
|
||||
case send_failed
|
||||
}
|
||||
|
||||
struct ZappingEvent {
|
||||
let is_custom: Bool
|
||||
let type: ZappingEventType
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
class ZapButtonModel: ObservableObject {
|
||||
var invoice: String? = nil
|
||||
@Published var zapping: String = ""
|
||||
@Published var showing_select_wallet: Bool = false
|
||||
@Published var showing_zap_customizer: Bool = false
|
||||
let target: ZapTarget
|
||||
}
|
||||
|
||||
struct ZapButton: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let target: ZapTarget
|
||||
let lnurl: String
|
||||
|
||||
@ObservedObject var zaps: ZapsDataModel
|
||||
@@ -45,11 +41,11 @@ struct ZapButton: View {
|
||||
var zap_img: String {
|
||||
switch our_zap {
|
||||
case .none:
|
||||
return "bolt"
|
||||
return "zap"
|
||||
case .zap:
|
||||
return "bolt.fill"
|
||||
return "zap.fill"
|
||||
case .pending:
|
||||
return "bolt.fill"
|
||||
return "zap.fill"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +67,7 @@ struct ZapButton: View {
|
||||
|
||||
func tap() {
|
||||
guard let our_zap else {
|
||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||
send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,9 +116,12 @@ struct ZapButton: View {
|
||||
HStack(spacing: 4) {
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(systemName: zap_img)
|
||||
Image(zap_img)
|
||||
.resizable()
|
||||
.foregroundColor(zap_color)
|
||||
.font(.footnote.weight(.medium))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width:20, height: 20)
|
||||
})
|
||||
|
||||
if zaps.zap_total > 0 {
|
||||
@@ -139,7 +138,7 @@ struct ZapButton: View {
|
||||
tap()
|
||||
})
|
||||
.sheet(isPresented: $button.showing_zap_customizer) {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
CustomizeZapView(state: damus_state, target: target, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
|
||||
@@ -147,7 +146,7 @@ struct ZapButton: View {
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard zap_ev.event.id == self.event.id else {
|
||||
guard zap_ev.target.id == self.target.id else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,6 +165,8 @@ struct ZapButton: View {
|
||||
let wallet = damus_state.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
}
|
||||
case .sent_from_nwc:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +178,7 @@ struct ZapButton_Previews: PreviewProvider {
|
||||
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||
let zaps = ZapsDataModel([.pending(pending_zap)])
|
||||
|
||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps)
|
||||
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,14 +194,13 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
|
||||
return .external(ExtPendingZapState(state: .fetching_invoice))
|
||||
}
|
||||
|
||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
guard let keypair = damus_state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
let content = comment ?? ""
|
||||
|
||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||
@@ -228,7 +228,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -242,7 +242,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -255,6 +255,9 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
// don't both continuing, user has canceled
|
||||
if case .cancel_fetching_invoice = nwc_state.state {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.canceled)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -273,6 +276,10 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
|
||||
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
||||
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
||||
|
||||
let typ = ZappingEventType.failed(.send_failed)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -281,9 +288,13 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
|
||||
// we don't need to trigger a ZapsDataModel update here
|
||||
}
|
||||
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
|
||||
notify(.zapping, ev)
|
||||
|
||||
case .external(let pending_ext):
|
||||
pending_ext.state = .done
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
@@ -291,7 +292,7 @@ struct ContentView: View {
|
||||
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."), systemImage: "line.3.horizontal.decrease")
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
@@ -315,6 +316,7 @@ struct ContentView: View {
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
setup_notifications()
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
@@ -454,6 +456,11 @@ struct ContentView: View {
|
||||
let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
if local.type == .profile_zap {
|
||||
open_profile(id: local.event_id)
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
@@ -469,6 +476,9 @@ struct ContentView: View {
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
// Handled separately above.
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
|
||||
@@ -692,7 +702,7 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
|
||||
let kinds = filter.kinds ?? []
|
||||
let initial: Int64? = nil
|
||||
let earliest = kinds.reduce(initial) { earliest, kind in
|
||||
let last = last_of_kind[kind]
|
||||
let last = last_of_kind[kind.rawValue]
|
||||
let since: Int64? = get_since_time(last_event: last)
|
||||
|
||||
if earliest == nil {
|
||||
@@ -746,10 +756,10 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
|
||||
|
||||
var has_event = false
|
||||
|
||||
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||
var filter = search_type == .event ? NostrFilter(ids: [evid]) : NostrFilter(authors: [evid])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [NostrKind.metadata.rawValue]
|
||||
filter.kinds = [.metadata]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
|
||||
@@ -24,7 +24,7 @@ class EventsModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func get_filter() -> NostrFilter {
|
||||
var filter = NostrFilter.filter_kinds([kind.rawValue])
|
||||
var filter = NostrFilter(kinds: [kind])
|
||||
filter.referenced_ids = [target]
|
||||
filter.limit = 500
|
||||
return filter
|
||||
|
||||
@@ -30,9 +30,8 @@ class FollowersModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var filter = NostrFilter.filter_contacts
|
||||
filter.pubkeys = [target]
|
||||
return filter
|
||||
NostrFilter(kinds: [.contacts],
|
||||
pubkeys: [target])
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
@@ -56,14 +55,13 @@ class FollowersModel: ObservableObject {
|
||||
}
|
||||
|
||||
func load_profiles(relay_id: String) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
|
||||
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
|
||||
if authors.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
filter.authors = authors
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata],
|
||||
authors: authors)
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ class FollowingModel {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
var f = NostrFilter(kinds: [.metadata])
|
||||
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
|
||||
// don't fetch profiles we already have
|
||||
if damus_state.profiles.lookup(id: pk) != nil {
|
||||
if damus_state.profiles.has_fresh_profile(id: pk) {
|
||||
return
|
||||
}
|
||||
acc.append(pk)
|
||||
|
||||
@@ -187,7 +187,12 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
switch zap.target {
|
||||
case .profile(let profile_id):
|
||||
create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id)
|
||||
case .note(let note_target):
|
||||
create_in_app_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,10 +402,9 @@ class HomeModel: ObservableObject {
|
||||
|
||||
/// Send the initial filters, just our contact list mostly
|
||||
func send_initial_filters(relay_id: String) {
|
||||
var filter = NostrFilter.filter_contacts
|
||||
filter.authors = [self.damus_state.pubkey]
|
||||
filter.limit = 1
|
||||
|
||||
var filter = NostrFilter(kinds: [.contacts],
|
||||
limit: 1,
|
||||
authors: [damus_state.pubkey])
|
||||
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
|
||||
}
|
||||
|
||||
@@ -411,23 +415,19 @@ class HomeModel: ObservableObject {
|
||||
var friends = damus_state.contacts.get_friend_list()
|
||||
friends.append(damus_state.pubkey)
|
||||
|
||||
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
var contacts_filter = NostrFilter(kinds: [.metadata])
|
||||
contacts_filter.authors = friends
|
||||
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
|
||||
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
|
||||
var our_blocklist_filter = NostrFilter(kinds: [.list])
|
||||
our_blocklist_filter.parameter = ["mute"]
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var dms_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.dm.rawValue,
|
||||
])
|
||||
var dms_filter = NostrFilter(kinds: [.dm])
|
||||
|
||||
var our_dms_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.dm.rawValue,
|
||||
])
|
||||
var our_dms_filter = NostrFilter(kinds: [.dm])
|
||||
|
||||
// friends only?...
|
||||
//dms_filter.authors = friends
|
||||
@@ -436,27 +436,27 @@ class HomeModel: ObservableObject {
|
||||
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.boost.rawValue
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text,
|
||||
.boost
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(NostrKind.like.rawValue)
|
||||
home_filter_kinds.append(.like)
|
||||
}
|
||||
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
|
||||
var home_filter = NostrFilter(kinds: home_filter_kinds)
|
||||
// include our pubkey as well even if we're not technically a friend
|
||||
home_filter.authors = friends
|
||||
home_filter.limit = 500
|
||||
|
||||
var notifications_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
var notifications_filter_kinds: [NostrKind] = [
|
||||
.text,
|
||||
.boost,
|
||||
.zap,
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
notifications_filter_kinds.append(NostrKind.like.rawValue)
|
||||
notifications_filter_kinds.append(.like)
|
||||
}
|
||||
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
|
||||
var notifications_filter = NostrFilter(kinds: notifications_filter_kinds)
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 500
|
||||
|
||||
@@ -1123,7 +1123,28 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else {
|
||||
print("Local notification scheduled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
@@ -1207,7 +1228,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
case .dm:
|
||||
title = displayName
|
||||
identifier = "myDMNotification"
|
||||
case .zap:
|
||||
case .zap, .profile_zap:
|
||||
// not handled here
|
||||
break
|
||||
}
|
||||
|
||||
@@ -69,16 +69,9 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var text_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
])
|
||||
var text_filter = NostrFilter(kinds: [.text, .chat])
|
||||
|
||||
var profile_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.contacts.rawValue,
|
||||
NostrKind.metadata.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class SearchHomeModel: ObservableObject {
|
||||
let base_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 250
|
||||
//let multiple_events_per_pubkey: Bool = false
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
@@ -27,7 +28,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_base_filter() -> NostrFilter {
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
|
||||
var filter = NostrFilter(kinds: [.text, .chat])
|
||||
filter.limit = self.limit
|
||||
filter.until = Int64(Date.now.timeIntervalSince1970)
|
||||
return filter
|
||||
@@ -60,7 +61,7 @@ class SearchHomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
|
||||
if seen_pubkey.contains(ev.pubkey) {
|
||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
seen_pubkey.insert(ev.pubkey)
|
||||
@@ -90,20 +91,6 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for pk in event_pubkeys {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
@@ -114,17 +101,7 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for pk in pks {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
|
||||
@@ -132,11 +109,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
|
||||
|
||||
for ev in events {
|
||||
// lookup profiles from boosted events
|
||||
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
|
||||
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
|
||||
pubkeys.insert(bev.pubkey)
|
||||
}
|
||||
|
||||
if profiles.lookup(id: ev.pubkey) == nil {
|
||||
if !profiles.has_fresh_profile(id: ev.pubkey) {
|
||||
pubkeys.insert(ev.pubkey)
|
||||
}
|
||||
}
|
||||
@@ -150,16 +127,16 @@ enum PubkeysToLoad {
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
|
||||
filter.authors = authors
|
||||
|
||||
guard !authors.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
print("loading \(authors.count) profiles from \(relay_id)")
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata],
|
||||
authors: authors)
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in
|
||||
let (sid, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: conn_ev) { sub_id, ev in
|
||||
guard sub_id == profiles_subid else {
|
||||
|
||||
@@ -34,7 +34,7 @@ class SearchModel: ObservableObject {
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
|
||||
search.kinds = [.text, .like]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
|
||||
@@ -56,16 +56,16 @@ class ThreadModel: ObservableObject {
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [NostrKind.text.rawValue]
|
||||
ref_events.kinds = [.text]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
|
||||
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
|
||||
var kinds: [NostrKind] = [.zap, .text, .boost]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
kinds.append(NostrKind.like.rawValue)
|
||||
kinds.append(.like)
|
||||
}
|
||||
meta_events.kinds = kinds
|
||||
|
||||
|
||||
@@ -137,6 +137,9 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
|
||||
@Setting(key: "onlyzaps_mode", default_value: false)
|
||||
var onlyzaps_mode: Bool
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ZapButtonModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapButtonModel: ObservableObject {
|
||||
var invoice: String? = nil
|
||||
@Published var zapping: String = ""
|
||||
@Published var showing_select_wallet: Bool = false
|
||||
@Published var showing_zap_customizer: Bool = false
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
|
||||
var filter = NostrFilter(kinds: [.zap])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
|
||||
<attribute name="about" optional="YES" attributeType="String"/>
|
||||
<attribute name="banner" optional="YES" attributeType="String"/>
|
||||
<attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="display_name" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lud06" optional="YES" attributeType="String"/>
|
||||
<attribute name="lud16" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="nip05" optional="YES" attributeType="String"/>
|
||||
<attribute name="picture" optional="YES" attributeType="String"/>
|
||||
<attribute name="website" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// PersistedProfile.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 4/30/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PersistedProfile)
|
||||
final class PersistedProfile: NSManagedObject {
|
||||
@NSManaged var id: String?
|
||||
@NSManaged var name: String?
|
||||
@NSManaged var display_name: String?
|
||||
@NSManaged var about: String?
|
||||
@NSManaged var picture: String?
|
||||
@NSManaged var banner: String?
|
||||
@NSManaged var website: String?
|
||||
@NSManaged var lud06: String?
|
||||
@NSManaged var lud16: String?
|
||||
@NSManaged var nip05: String?
|
||||
@NSManaged var damus_donation: Int16
|
||||
@NSManaged var last_update: Date? // The date that the profile was last updated by the user
|
||||
@NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
|
||||
|
||||
func copyValues(from profile: Profile) {
|
||||
name = profile.name
|
||||
display_name = profile.display_name
|
||||
about = profile.about
|
||||
picture = profile.picture
|
||||
banner = profile.banner
|
||||
website = profile.website
|
||||
lud06 = profile.lud06
|
||||
lud16 = profile.lud16
|
||||
nip05 = profile.nip05
|
||||
damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,19 @@ class Profile: Codable {
|
||||
self.damus_donation = damus_donation
|
||||
}
|
||||
|
||||
convenience init(persisted_profile: PersistedProfile) {
|
||||
self.init(name: persisted_profile.name,
|
||||
display_name: persisted_profile.display_name,
|
||||
about: persisted_profile.about,
|
||||
picture: persisted_profile.picture,
|
||||
banner: persisted_profile.banner,
|
||||
website: persisted_profile.website,
|
||||
lud06: persisted_profile.lud06,
|
||||
lud16: persisted_profile.lud16,
|
||||
nip05: persisted_profile.nip05,
|
||||
damus_donation: Int(persisted_profile.damus_donation))
|
||||
}
|
||||
|
||||
private func str(_ str: String) -> String? {
|
||||
return get_val(str)
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ import Foundation
|
||||
|
||||
struct NostrFilter: Codable, Equatable {
|
||||
var ids: [String]?
|
||||
var kinds: [Int]?
|
||||
var kinds: [NostrKind]?
|
||||
var referenced_ids: [String]?
|
||||
var pubkeys: [String]?
|
||||
var since: Int64?
|
||||
var until: Int64?
|
||||
var limit: UInt32?
|
||||
var authors: [String]?
|
||||
var hashtag: [String]? = nil
|
||||
var parameter: [String]? = nil
|
||||
var hashtag: [String]?
|
||||
var parameter: [String]?
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case ids
|
||||
@@ -32,31 +32,23 @@ struct NostrFilter: Codable, Equatable {
|
||||
case limit
|
||||
}
|
||||
|
||||
init(ids: [String]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [String]? = nil, pubkeys: [String]? = nil, since: Int64? = nil, until: Int64? = nil, limit: UInt32? = nil, authors: [String]? = nil, hashtag: [String]? = nil) {
|
||||
self.ids = ids
|
||||
self.kinds = kinds
|
||||
self.referenced_ids = referenced_ids
|
||||
self.pubkeys = pubkeys
|
||||
self.since = since
|
||||
self.until = until
|
||||
self.limit = limit
|
||||
self.authors = authors
|
||||
self.hashtag = hashtag
|
||||
}
|
||||
|
||||
public static func copy(from: NostrFilter) -> NostrFilter {
|
||||
return NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
|
||||
NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
|
||||
}
|
||||
|
||||
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() })
|
||||
}
|
||||
|
||||
public static func filter_ids(_ ids: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
|
||||
}
|
||||
|
||||
public static var filter_profiles: NostrFilter {
|
||||
return filter_kinds([NostrKind.metadata.rawValue])
|
||||
}
|
||||
|
||||
public static var filter_contacts: NostrFilter {
|
||||
return filter_kinds([NostrKind.contacts.rawValue])
|
||||
}
|
||||
|
||||
public static func filter_authors(_ authors: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: authors)
|
||||
}
|
||||
|
||||
public static func filter_kinds(_ kinds: [Int]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: kinds, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
|
||||
NostrFilter(hashtag: htags.map { $0.lowercased() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
enum NostrKind: Int {
|
||||
enum NostrKind: Int, Codable {
|
||||
case metadata = 0
|
||||
case text = 1
|
||||
case contacts = 3
|
||||
|
||||
@@ -140,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
}
|
||||
|
||||
if tag_is_hashtag(parts) {
|
||||
return .filter(NostrFilter.filter_hashtag([parts[1]]))
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
}
|
||||
|
||||
if let rid = tag_to_refid(parts) {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// ProfileDatabase.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 4/30/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
enum ProfileDatabaseError: Error {
|
||||
case missing_context
|
||||
case outdated_input
|
||||
}
|
||||
|
||||
final class ProfileDatabase {
|
||||
|
||||
private let entity_name = "PersistedProfile"
|
||||
private var persistent_container: NSPersistentContainer?
|
||||
private var background_context: NSManagedObjectContext?
|
||||
private let cache_url: URL
|
||||
|
||||
/// This queue is used to synchronize access to the network_pull_date_cache dictionary, which
|
||||
/// prevents data races from crashing the app.
|
||||
private var queue = DispatchQueue(label: "io.damus.profile_db",
|
||||
qos: .userInteractive,
|
||||
attributes: .concurrent)
|
||||
private var network_pull_date_cache = [String: Date]()
|
||||
|
||||
init(cache_url: URL = ProfileDatabase.profile_cache_url) {
|
||||
self.cache_url = cache_url
|
||||
set_up()
|
||||
}
|
||||
|
||||
private static var profile_cache_url: URL {
|
||||
(FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
|
||||
}
|
||||
|
||||
private var persistent_store_description: NSPersistentStoreDescription {
|
||||
let description = NSPersistentStoreDescription(url: cache_url)
|
||||
description.type = NSSQLiteStoreType
|
||||
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
|
||||
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
|
||||
description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
|
||||
return description
|
||||
}
|
||||
|
||||
private var object_model: NSManagedObjectModel? {
|
||||
guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
|
||||
return nil
|
||||
}
|
||||
return NSManagedObjectModel(contentsOf: url)
|
||||
}
|
||||
|
||||
private func set_up() {
|
||||
guard let object_model else {
|
||||
print("⚠️ Warning: ProfileDatabase failed to load its object model")
|
||||
return
|
||||
}
|
||||
|
||||
persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
|
||||
persistent_container?.persistentStoreDescriptions = [persistent_store_description]
|
||||
persistent_container?.loadPersistentStores { _, error in
|
||||
if let error {
|
||||
print("WARNING: ProfileDatabase failed to load: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
|
||||
persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
|
||||
|
||||
background_context = persistent_container?.newBackgroundContext()
|
||||
background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
|
||||
}
|
||||
|
||||
private func get_persisted(id: String, context: NSManagedObjectContext) -> PersistedProfile? {
|
||||
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
|
||||
request.predicate = NSPredicate(format: "id == %@", id)
|
||||
request.fetchLimit = 1
|
||||
return try? context.fetch(request).first
|
||||
}
|
||||
|
||||
func get_network_pull_date(id: String) -> Date? {
|
||||
var pull_date: Date?
|
||||
queue.sync {
|
||||
pull_date = network_pull_date_cache[id]
|
||||
}
|
||||
if let pull_date {
|
||||
return pull_date
|
||||
}
|
||||
|
||||
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
|
||||
request.predicate = NSPredicate(format: "id == %@", id)
|
||||
request.fetchLimit = 1
|
||||
request.propertiesToFetch = ["network_pull_date"]
|
||||
guard let profile = try? persistent_container?.viewContext.fetch(request).first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
queue.async(flags: .barrier) {
|
||||
self.network_pull_date_cache[id] = profile.network_pull_date
|
||||
}
|
||||
return profile.network_pull_date
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
|
||||
/// is older than one we already have. Database writes occur on a background context for best performance.
|
||||
/// - Parameters:
|
||||
/// - id: Profile id (pubkey)
|
||||
/// - profile: Profile object to be stored
|
||||
/// - last_update: Date that the Profile was updated
|
||||
func upsert(id: String, profile: Profile, last_update: Date) async throws {
|
||||
guard let context = background_context else {
|
||||
throw ProfileDatabaseError.missing_context
|
||||
}
|
||||
|
||||
try await context.perform {
|
||||
var persisted_profile: PersistedProfile?
|
||||
if let profile = self.get_persisted(id: id, context: context) {
|
||||
if let existing_last_update = profile.last_update, last_update < existing_last_update {
|
||||
throw ProfileDatabaseError.outdated_input
|
||||
} else {
|
||||
persisted_profile = profile
|
||||
}
|
||||
} else {
|
||||
persisted_profile = NSEntityDescription.insertNewObject(forEntityName: self.entity_name, into: context) as? PersistedProfile
|
||||
persisted_profile?.id = id
|
||||
}
|
||||
persisted_profile?.copyValues(from: profile)
|
||||
persisted_profile?.last_update = last_update
|
||||
|
||||
let pull_date = Date.now
|
||||
persisted_profile?.network_pull_date = pull_date
|
||||
self.queue.async(flags: .barrier) {
|
||||
self.network_pull_date_cache[id] = pull_date
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
|
||||
func get(id: String) -> Profile? {
|
||||
guard let container = persistent_container,
|
||||
let profile = get_persisted(id: id, context: container.viewContext) else {
|
||||
return nil
|
||||
}
|
||||
return Profile(persisted_profile: profile)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
|
||||
let count = try? persistent_container?.viewContext.count(for: request)
|
||||
return count ?? 0
|
||||
}
|
||||
|
||||
func remove_all_profiles() throws {
|
||||
guard let context = background_context, let container = persistent_container else {
|
||||
throw ProfileDatabaseError.missing_context
|
||||
}
|
||||
|
||||
queue.async(flags: .barrier) {
|
||||
self.network_pull_date_cache.removeAll()
|
||||
}
|
||||
|
||||
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity_name)
|
||||
let batch_delete_request = NSBatchDeleteRequest(fetchRequest: request)
|
||||
batch_delete_request.resultType = .resultTypeObjectIDs
|
||||
|
||||
let result = try container.persistentStoreCoordinator.execute(batch_delete_request, with: context) as! NSBatchDeleteResult
|
||||
|
||||
// NSBatchDeleteRequest is an NSPersistentStoreRequest, which operates on disk. So now we'll manually update our in-memory context.
|
||||
if let object_ids = result.result as? [NSManagedObjectID] {
|
||||
let changes: [AnyHashable: Any] = [
|
||||
NSDeletedObjectsKey: object_ids
|
||||
]
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
||||
class Profiles {
|
||||
|
||||
static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
|
||||
|
||||
/// This queue is used to synchronize access to the profiles dictionary, which
|
||||
/// prevents data races from crashing the app.
|
||||
private var queue = DispatchQueue(label: "io.damus.profiles",
|
||||
@@ -22,8 +22,10 @@ class Profiles {
|
||||
var nip05_pubkey: [String: String] = [:]
|
||||
var zappers: [String: String] = [:]
|
||||
|
||||
private let database = ProfileDatabase()
|
||||
|
||||
func is_validated(_ pk: String) -> NIP05? {
|
||||
return validated[pk]
|
||||
validated[pk]
|
||||
}
|
||||
|
||||
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
|
||||
@@ -33,23 +35,29 @@ class Profiles {
|
||||
}
|
||||
|
||||
func lookup_zapper(pubkey: String) -> String? {
|
||||
if let zapper = zappers[pubkey] {
|
||||
return zapper
|
||||
}
|
||||
|
||||
return nil
|
||||
zappers[pubkey]
|
||||
}
|
||||
|
||||
func add(id: String, profile: TimestampedProfile) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.profiles[id] = profile
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await database.upsert(id: id, profile: profile.profile, last_update: Date(timeIntervalSince1970: TimeInterval(profile.timestamp)))
|
||||
} catch {
|
||||
print("⚠️ Warning: Profiles failed to save a profile: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lookup(id: String) -> Profile? {
|
||||
var profile: Profile?
|
||||
queue.sync {
|
||||
return profiles[id]?.profile
|
||||
profile = profiles[id]?.profile
|
||||
}
|
||||
return profile ?? database.get(id: id)
|
||||
}
|
||||
|
||||
func lookup_with_timestamp(id: String) -> TimestampedProfile? {
|
||||
@@ -57,6 +65,23 @@ class Profiles {
|
||||
return profiles[id]
|
||||
}
|
||||
}
|
||||
|
||||
func has_fresh_profile(id: String) -> Bool {
|
||||
// check memory first
|
||||
var profile: Profile?
|
||||
queue.sync {
|
||||
profile = profiles[id]?.profile
|
||||
}
|
||||
if profile != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// then disk
|
||||
guard let pull_date = database.get_network_pull_date(id: id) else {
|
||||
return false
|
||||
}
|
||||
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ class RelativeTimeModel: ObservableObject {
|
||||
@Published var value: String = ""
|
||||
}
|
||||
|
||||
class MediaMetaModel: ObservableObject {
|
||||
@Published var fill: ImageFill? = nil
|
||||
}
|
||||
|
||||
class EventData {
|
||||
var translations_model: TranslationModel
|
||||
var artifacts_model: NoteArtifactsModel
|
||||
@@ -108,6 +112,7 @@ class EventData {
|
||||
var zaps_model : ZapsDataModel
|
||||
var relative_time: RelativeTimeModel = RelativeTimeModel()
|
||||
var validated: ValidationResult
|
||||
var media_metadata_model: MediaMetaModel
|
||||
|
||||
var translations: TranslateStatus {
|
||||
return translations_model.state
|
||||
@@ -126,6 +131,7 @@ class EventData {
|
||||
self.artifacts_model = .init(state: .not_loaded)
|
||||
self.zaps_model = .init(zaps)
|
||||
self.validated = .unknown
|
||||
self.media_metadata_model = MediaMetaModel()
|
||||
self.preview_model = .init(state: .not_loaded)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +141,7 @@ class EventCache {
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var image_metadata: [String: ImageMetadataState] = [:]
|
||||
private var video_meta: [String: VideoPlayerModel] = [:]
|
||||
private var event_data: [String: EventData] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
@@ -194,6 +201,28 @@ class EventCache {
|
||||
return image_metadata[url.absoluteString.lowercased()]
|
||||
}
|
||||
|
||||
func lookup_media_size(url: URL) -> CGSize? {
|
||||
if let img_meta = lookup_img_metadata(url: url) {
|
||||
return img_meta.meta.dim?.size
|
||||
}
|
||||
|
||||
return get_video_player_model(url: url).size
|
||||
}
|
||||
|
||||
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
|
||||
video_meta[url.absoluteString] = meta
|
||||
}
|
||||
|
||||
func get_video_player_model(url: URL) -> VideoPlayerModel {
|
||||
if let model = video_meta[url.absoluteString] {
|
||||
return model
|
||||
}
|
||||
|
||||
let model = VideoPlayerModel()
|
||||
video_meta[url.absoluteString] = model
|
||||
return model
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
@@ -257,6 +286,7 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
video_meta = [:]
|
||||
event_data = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
@@ -370,6 +400,14 @@ func preload_image(url: URL) {
|
||||
}
|
||||
}
|
||||
|
||||
func is_animated_image(url: URL) -> Bool {
|
||||
guard let ext = url.pathComponents.last?.split(separator: ".").last?.lowercased() else {
|
||||
return false
|
||||
}
|
||||
|
||||
return ext == "gif"
|
||||
}
|
||||
|
||||
func preload_event(plan: PreloadPlan, state: DamusState) async {
|
||||
var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts
|
||||
let settings = state.settings
|
||||
@@ -388,6 +426,13 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
|
||||
}
|
||||
|
||||
for url in arts.images {
|
||||
guard !is_animated_image(url: url) else {
|
||||
// jb55: I have a theory that animated images are not working with the preloader due
|
||||
// to some disk-cache write race condition. normal images need not apply
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
preload_image(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,5 @@ enum LocalNotificationType: String {
|
||||
case mention
|
||||
case repost
|
||||
case zap
|
||||
case profile_zap
|
||||
}
|
||||
|
||||
@@ -66,22 +66,12 @@ enum PreviewState {
|
||||
|
||||
class PreviewCache {
|
||||
private var previews: [String: Preview]
|
||||
private var image_meta: [String: ImageFill]
|
||||
|
||||
func lookup(_ evid: String) -> Preview? {
|
||||
return previews[evid]
|
||||
}
|
||||
|
||||
func lookup_image_meta(_ evid: String) -> ImageFill? {
|
||||
return image_meta[evid]
|
||||
}
|
||||
|
||||
func cache_image_meta(evid: String, image_fill: ImageFill) {
|
||||
self.image_meta[evid] = image_fill
|
||||
}
|
||||
|
||||
init() {
|
||||
self.previews = [:]
|
||||
self.image_meta = [:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypai
|
||||
}
|
||||
|
||||
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
||||
var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue])
|
||||
var filter = NostrFilter(kinds: [.nwc_response])
|
||||
filter.authors = [url.pubkey]
|
||||
filter.limit = 0
|
||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||
|
||||
@@ -43,7 +43,7 @@ struct EventActionBar: View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.compose, PostAction.replying_to(event))
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
@@ -55,7 +55,7 @@ struct EventActionBar: View {
|
||||
Spacer()
|
||||
HStack(spacing: 4) {
|
||||
|
||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
|
||||
if bar.boosted {
|
||||
notify(.delete, bar.our_boost)
|
||||
} else {
|
||||
@@ -88,11 +88,11 @@ struct EventActionBar: View {
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
||||
ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
|
||||
EventActionButton(img: "upload", col: Color.gray) {
|
||||
show_share_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
|
||||
@@ -161,9 +161,12 @@ struct EventActionBar: View {
|
||||
|
||||
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: img)
|
||||
Image(img)
|
||||
.resizable()
|
||||
.foregroundColor(col == nil ? Color.gray : col!)
|
||||
.font(.footnote.weight(.medium))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,11 +191,16 @@ struct LikeButton: View {
|
||||
}) {
|
||||
if liked {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("shaka-full")
|
||||
.mask(Image("shaka.fill")
|
||||
.resizable()
|
||||
).frame(width: 14, height: 14)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
)
|
||||
.frame(width: 20, height: 20)
|
||||
} else {
|
||||
Image("shaka-line")
|
||||
Image("shaka")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ struct RepostAction: View {
|
||||
|
||||
damus_state.postbox.send(boost)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), systemImage: "arrow.2.squarepath")
|
||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
||||
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ struct ShareAction: View {
|
||||
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
|
||||
}
|
||||
|
||||
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
|
||||
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
|
||||
ShareActionButton(img: bookmarkImg, text: bookmarkTxt) {
|
||||
dismiss()
|
||||
@@ -54,7 +54,7 @@ struct ShareAction: View {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
|
||||
ShareActionButton(img: "upload", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
|
||||
show_share = true
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ struct ShareActionButton: View {
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack() {
|
||||
Image(systemName: img)
|
||||
Image(img)
|
||||
.foregroundColor(col)
|
||||
.font(.system(size: 23, weight: .bold))
|
||||
.overlay {
|
||||
@@ -48,6 +48,6 @@ struct ShareActionButton: View {
|
||||
|
||||
struct ShareActionButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShareActionButton(img: "figure.flexibility", text: "Stretch", action: {})
|
||||
ShareActionButton(img: "link", text: "Stretch", action: {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct AddRelayView: View {
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
Label("", systemImage: "xmark.circle.fill")
|
||||
Label("", image: "close-circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.trailing, -25.0)
|
||||
.opacity((relay == "") ? 0.0 : 1.0)
|
||||
@@ -28,7 +28,7 @@ struct AddRelayView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Label("", systemImage: "doc.on.clipboard")
|
||||
Label("", image: "copy2")
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
if let pastedrelay = UIPasteboard.general.string {
|
||||
|
||||
@@ -29,7 +29,7 @@ struct BookmarksView: View {
|
||||
Group {
|
||||
if bookmarks.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "bookmark")
|
||||
Image("bookmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
|
||||
@@ -21,13 +21,13 @@ struct FriendsButton: View {
|
||||
}) {
|
||||
if filter == .friends {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image(systemName: "person.2.fill")
|
||||
.mask(Image("user-added")
|
||||
.resizable()
|
||||
).frame(width: 30, height: 20)
|
||||
).frame(width: 28, height: 28)
|
||||
} else {
|
||||
Image(systemName: "person.2.fill")
|
||||
Image("user-added")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 20)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,23 +37,27 @@ struct ConfigView: View {
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple)
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key", color: .purple)
|
||||
}
|
||||
|
||||
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
|
||||
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "eye", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(destination: SearchSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "magnifyingglass", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "bell.fill", color: .blue)
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
|
||||
}
|
||||
|
||||
NavigationLink(destination: ZapSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange)
|
||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
|
||||
}
|
||||
|
||||
NavigationLink(destination: TranslationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe.americas.fill", color: .green)
|
||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,20 +70,25 @@ struct ConfigView: View {
|
||||
confirm_logout = true
|
||||
}
|
||||
}, label: {
|
||||
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
|
||||
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), image: "logout")
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
})
|
||||
}
|
||||
|
||||
if state.is_privkey_user {
|
||||
Section(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
||||
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
|
||||
Section(header: Text(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user"))) {
|
||||
Button(action: {
|
||||
delete_account_warning = true
|
||||
}
|
||||
}, label: {
|
||||
Label(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), image: "delete")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.red)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
|
||||
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
|
||||
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
|
||||
|
||||
@@ -25,70 +25,63 @@ struct CreateAccountView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
DamusGradient()
|
||||
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
VStack {
|
||||
Text("Create Account")
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
|
||||
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
|
||||
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
Text(verbatim: " ")
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
VStack {
|
||||
SignupForm {
|
||||
FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry."))
|
||||
HStack(spacing: 0.0) {
|
||||
Text(verbatim: "@")
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, -25.0)
|
||||
|
||||
FormTextInput(NSLocalizedString("satoshi", comment: "Example username of Bitcoin creator(s), Satoshi Nakamoto."), text: $account.nick_name)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
}
|
||||
|
||||
FormLabel(NSLocalizedString("Display Name", comment: "Label to prompt display name entry."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
|
||||
.textInputAutocapitalization(.words)
|
||||
|
||||
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
|
||||
|
||||
FormLabel(NSLocalizedString("Account ID", comment: "Label to indicate the public ID of the account."))
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
|
||||
KeyText($account.pubkey)
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
VStack(alignment: .center) {
|
||||
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
|
||||
|
||||
Text(NSLocalizedString("Public Key", comment: "Label to indicate the public key of the account."))
|
||||
.bold()
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
}
|
||||
|
||||
KeyText($account.pubkey)
|
||||
.padding(.horizontal, 20)
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, alignment: .center)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1)
|
||||
}
|
||||
|
||||
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
|
||||
EmptyView()
|
||||
SignupForm {
|
||||
FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
|
||||
.textInputAutocapitalization(.words)
|
||||
|
||||
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
|
||||
}
|
||||
|
||||
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) {
|
||||
.padding(.top, 10)
|
||||
|
||||
Button(action: {
|
||||
self.is_done = true
|
||||
}) {
|
||||
HStack {
|
||||
Text("Create account now", comment: "Button to create account.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.padding()
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.disabled(profileUploadViewModel.isLoading)
|
||||
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
|
||||
.padding(.top, 20)
|
||||
|
||||
LoginPrompt()
|
||||
}
|
||||
.padding(.leading, 14.0)
|
||||
.padding(.trailing, 20.0)
|
||||
|
||||
.padding()
|
||||
}
|
||||
.dismissKeyboardOnTap()
|
||||
.navigationTitle("Create account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
@@ -99,12 +92,27 @@ struct CreateAccountView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginPrompt: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Already on nostr?", comment: "Ask the user if they already have an account on nostr")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BackNav: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "chevron.backward")
|
||||
.foregroundColor(.white)
|
||||
Image("chevron-left")
|
||||
.foregroundColor(.white)
|
||||
.onTapGesture {
|
||||
self.dismiss()
|
||||
}
|
||||
@@ -136,20 +144,21 @@ func KeyText(_ text: Binding<String>) -> some View {
|
||||
let bechkey = bech32_encode(hrp: PUBKEY_HRP, decoded)
|
||||
return Text(bechkey)
|
||||
.textSelection(.enabled)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout.monospaced())
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
}
|
||||
|
||||
func FormTextInput(_ title: String, text: Binding<String>) -> some View {
|
||||
return TextField("", text: text)
|
||||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||||
Text(title).foregroundColor(.white.opacity(0.4))
|
||||
Text(title).foregroundColor(.gray.opacity(0.5))
|
||||
}
|
||||
.padding()
|
||||
.padding(15)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.body.bold())
|
||||
}
|
||||
|
||||
@@ -157,11 +166,10 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
|
||||
return HStack {
|
||||
Text(title)
|
||||
.bold()
|
||||
.foregroundColor(.white)
|
||||
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(.white.opacity(0.5))
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ struct DMChatView: View, KeyboardReadable {
|
||||
}
|
||||
}
|
||||
) {
|
||||
Label("", systemImage: "arrow.right.circle")
|
||||
Label("", image: "send")
|
||||
.font(.title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,28 +7,14 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EULAView: View {
|
||||
var state: SetupState?
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var accepted = false
|
||||
let eula = """
|
||||
**End User License Agreement**
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
DamusGradient()
|
||||
|
||||
ScrollView {
|
||||
Text("EULA", comment: "Label indicating that the below text is the EULA, an acronym for End User License Agreement.")
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(Markdown.parse(content: """
|
||||
End User License Agreement
|
||||
|
||||
## Introduction
|
||||
**Introduction**
|
||||
|
||||
This End User License Agreement ("EULA") is a legal agreement between you and Damus Nostr Inc. for the use of our mobile application Damus. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA.
|
||||
|
||||
## Prohibited Content and Conduct
|
||||
**Prohibited Content and Conduct**
|
||||
|
||||
You agree not to use our application to create, upload, post, send, or store any content that:
|
||||
|
||||
@@ -40,59 +26,101 @@ You agree not to use our application to create, upload, post, send, or store any
|
||||
* Is intended to harass or bully others
|
||||
* Is intended to impersonate others
|
||||
|
||||
## You also agree not to engage in any conduct that:
|
||||
**You also agree not to engage in any conduct that:**
|
||||
|
||||
* Harasses or bullies others
|
||||
* Impersonates others
|
||||
* Is intended to intimidate or threaten others
|
||||
* Is intended to promote or incite violence
|
||||
|
||||
## Consequences of Violation
|
||||
**Consequences of Violation**
|
||||
|
||||
Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application.
|
||||
|
||||
## Disclaimer of Warranties and Limitation of Liability
|
||||
**Disclaimer of Warranties and Limitation of Liability**
|
||||
|
||||
Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Damus Nostr Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application.
|
||||
|
||||
## Changes to EULA
|
||||
**Changes to EULA**
|
||||
|
||||
We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes.
|
||||
|
||||
## Contact Information
|
||||
**Contact Information**
|
||||
|
||||
If you have any questions about this EULA, please contact us at damus@jb55.com
|
||||
|
||||
## Acceptance of Terms
|
||||
**Acceptance of Terms**
|
||||
|
||||
By using our Application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our Application.
|
||||
|
||||
"""))
|
||||
.padding()
|
||||
|
||||
if state == .create_account {
|
||||
NavigationLink(destination: CreateAccountView(), isActive: $accepted) {
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: LoginView(), isActive: $accepted) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
DamusWhiteButton(NSLocalizedString("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")) {
|
||||
accepted = true
|
||||
}
|
||||
"""
|
||||
|
||||
DamusWhiteButton(NSLocalizedString("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")) {
|
||||
dismiss()
|
||||
struct EULAView: View {
|
||||
@State private var login = false
|
||||
@State var accepted = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
NavigationLink(destination: LoginView(accepted: $accepted), isActive: $login) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Text(Markdown.parse(content: eula))
|
||||
.padding()
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 10, bottom: 50, trailing: 10))
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 75, maxHeight: 12, alignment: .center)
|
||||
.padding()
|
||||
.foregroundColor(Color.white)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(DamusColors.darkGrey, strokeBorder: DamusColors.mediumGrey, lineWidth: 1)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
accepted = true
|
||||
login.toggle()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 75, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
}
|
||||
.padding(.trailing, 30)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(
|
||||
Image("eula-bg")
|
||||
.resizable()
|
||||
.blur(radius: 70)
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
.navigationTitle("EULA")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
struct EmptyTimelineView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "tray.fill")
|
||||
Image("question")
|
||||
.font(.system(size: 35))
|
||||
.padding()
|
||||
Text("Nothing to see here. Check back later!", comment: "Indicates that there are no notes in the timeline to view.")
|
||||
|
||||
@@ -17,7 +17,7 @@ import SwiftUI
|
||||
struct EmptyUserSearchView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
Image("question")
|
||||
.font(.system(size: 35))
|
||||
.padding()
|
||||
Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.")
|
||||
|
||||
@@ -12,6 +12,7 @@ enum EventViewKind {
|
||||
case small
|
||||
case normal
|
||||
case selected
|
||||
case subheadline
|
||||
}
|
||||
|
||||
struct EventView: View {
|
||||
@@ -73,7 +74,7 @@ extension View {
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_pubkey
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,6 +107,8 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
return .body
|
||||
case .selected:
|
||||
return .custom("selected", size: 21.0)
|
||||
case .subheadline:
|
||||
return .subheadline
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +120,8 @@ func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .selected:
|
||||
return .preferredFont(forTextStyle: .title2)
|
||||
case .subheadline:
|
||||
return .preferredFont(forTextStyle: .subheadline)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ struct BuilderEventView: View {
|
||||
subscribe(filters: [
|
||||
NostrFilter(ids: [self.event_id], limit: 1),
|
||||
NostrFilter(
|
||||
kinds: [NostrKind.zap.rawValue],
|
||||
kinds: [.zap],
|
||||
referenced_ids: [self.event_id]
|
||||
)
|
||||
])
|
||||
|
||||
@@ -60,25 +60,25 @@ struct MenuItems: View {
|
||||
Button {
|
||||
UIPasteboard.general.string = event.get_content(keypair.privkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
|
||||
Label(NSLocalizedString("Copy user public key", comment: "Context menu option for copying the ID of the user who created the note."), image: "user")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
|
||||
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = event_to_json(ev: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
|
||||
Label(NSLocalizedString("Copy note JSON", comment: "Context menu option for copying the JSON text from the note."), image: "code.on.square")
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -86,9 +86,9 @@ struct MenuItems: View {
|
||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
||||
} label: {
|
||||
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.")
|
||||
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.")
|
||||
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
|
||||
let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.")
|
||||
let addBookmarkString = NSLocalizedString("Add bookmark", comment: "Context menu option for adding a note bookmark.")
|
||||
Label(isBookmarked ? removeBookmarkString : addBookmarkString, image: imageName)
|
||||
}
|
||||
|
||||
if event.known_kind != .dm {
|
||||
@@ -97,17 +97,17 @@ struct MenuItems: View {
|
||||
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
|
||||
isMutedThread = muted
|
||||
} label: {
|
||||
let imageName = isMutedThread ? "speaker" : "speaker.slash"
|
||||
let imageName = isMutedThread ? "mute" : "mute"
|
||||
let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.")
|
||||
let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.")
|
||||
Label(isMutedThread ? unmuteThreadString : muteThreadString, systemImage: imageName)
|
||||
Label(isMutedThread ? unmuteThreadString : muteThreadString, image: imageName)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
|
||||
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), image: "globe")
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
@@ -116,13 +116,13 @@ struct MenuItems: View {
|
||||
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
|
||||
notify(.report, target)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
|
||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
notify(.mute, target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Mute User", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
|
||||
Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ func eventview_pfp_size(_ size: EventViewKind) -> CGFloat {
|
||||
return PFP_SIZE
|
||||
case .selected:
|
||||
return PFP_SIZE
|
||||
case .subheadline:
|
||||
return PFP_SIZE * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ struct ZapEvent: View {
|
||||
.padding([.top], 2)
|
||||
|
||||
if zap.is_private {
|
||||
Image(systemName: "lock.fill")
|
||||
Image("lock")
|
||||
.foregroundColor(DamusColors.green)
|
||||
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import Kingfisher
|
||||
|
||||
|
||||
struct ImageContainerView: View {
|
||||
let url: URL?
|
||||
let cache: EventCache
|
||||
let url: MediaUrl
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
@@ -26,8 +27,7 @@ struct ImageContainerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
func Img(url: URL) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note, disable_animation: disable_animation)
|
||||
.configure { view in
|
||||
@@ -40,12 +40,23 @@ struct ImageContainerView: View {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch url {
|
||||
case .image(let url):
|
||||
Img(url: url)
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: cache.get_video_player_model(url: url), video_size: .constant(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
|
||||
|
||||
struct ImageContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageContainerView(url: test_image_url, disable_animation: false)
|
||||
ImageContainerView(cache: test_damus_state().events, url: .image(test_image_url), disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,24 +19,24 @@ struct ImageContextMenuModifier: ViewModifier {
|
||||
Button {
|
||||
UIPasteboard.general.url = url
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), image: "copy2")
|
||||
}
|
||||
if let someImage = image {
|
||||
Button {
|
||||
UIPasteboard.general.image = someImage
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
|
||||
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), image: "copy2.fill")
|
||||
}
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
|
||||
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), image: "download")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
showShareSheet = true
|
||||
} label: {
|
||||
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
|
||||
Label(NSLocalizedString("Share", comment: "Button to share an image."), image: "upload")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
|
||||
let urls: [URL?]
|
||||
let cache: EventCache
|
||||
let urls: [MediaUrl]
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@@ -39,7 +39,7 @@ struct ImageView: View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: urls[index], disable_animation: disable_animation)
|
||||
ImageContainerView(cache: cache, url: urls[index], disable_animation: disable_animation)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
@@ -79,6 +79,7 @@ struct ImageView: View {
|
||||
|
||||
struct ImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false)
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageView(cache: test_damus_state().events, urls: [url], disable_animation: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ struct NavDismissBarView: View {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark")
|
||||
Image("close")
|
||||
.frame(width: 33, height: 33)
|
||||
.background(.regularMaterial)
|
||||
.clipShape(Circle())
|
||||
|
||||
@@ -33,10 +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()
|
||||
|
||||
@Binding var accepted: Bool
|
||||
|
||||
func get_error(parsed_key: ParsedKey?) -> String? {
|
||||
if self.error != nil {
|
||||
@@ -52,27 +55,22 @@ struct LoginView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
DamusGradient()
|
||||
if accepted {
|
||||
NavigationLink(destination: CreateAccountView(), isActive: $create_account) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Text("Login", comment: "Title of view to log into an account.")
|
||||
.foregroundColor(.white)
|
||||
.font(.title)
|
||||
.padding()
|
||||
|
||||
Text("Enter your account key to login:", comment: "Prompt for user to enter an account key to login.")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
|
||||
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: $key)
|
||||
|
||||
SignInHeader()
|
||||
.padding(.top, 100)
|
||||
|
||||
SignInEntry(key: $key)
|
||||
|
||||
let parsed = parse_key(key)
|
||||
|
||||
|
||||
if parsed?.is_hex ?? false {
|
||||
Text("This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.", comment: "Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key.")
|
||||
.font(.subheadline.bold())
|
||||
.foregroundColor(.white)
|
||||
PubkeySwitch(isOn: $is_pubkey)
|
||||
.padding()
|
||||
// convert to bech32 here
|
||||
}
|
||||
|
||||
if let error = get_error(parsed_key: parsed) {
|
||||
@@ -83,14 +81,13 @@ struct LoginView: View {
|
||||
|
||||
if parsed?.is_pub ?? false {
|
||||
Text("This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.", comment: "Warning that the inputted account key is a public key and the result of what happens because of it.")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.foregroundColor(Color.orange)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let p = parsed {
|
||||
DamusWhiteButton(NSLocalizedString("Login", comment: "Button to log into account.")) {
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
do {
|
||||
try await process_login(p, is_pubkey: is_pubkey)
|
||||
@@ -98,11 +95,31 @@ struct LoginView: View {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Login", comment: "Button to log into account.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 10)
|
||||
}
|
||||
|
||||
CreateAccountPrompt(create_account: $create_account)
|
||||
.padding(.top, 10)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(
|
||||
Image("login-header")
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: 350, alignment: .center)
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
.onAppear {
|
||||
credential_handler.check_credentials()
|
||||
}
|
||||
@@ -111,18 +128,6 @@ struct LoginView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct PubkeySwitch: View {
|
||||
@Binding var isOn: Bool
|
||||
var body: some View {
|
||||
HStack {
|
||||
Toggle(isOn: $isOn) {
|
||||
Text("Public Key?", comment: "Prompt to ask user if the key they entered is a public key.")
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parse_key(_ thekey: String) -> ParsedKey? {
|
||||
var key = thekey
|
||||
if key.count > 0 && key.first! == "@" {
|
||||
@@ -270,39 +275,90 @@ struct KeyInput: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
.foregroundColor(.gray)
|
||||
.onTapGesture {
|
||||
if let pastedkey = UIPasteboard.general.string {
|
||||
self.key.wrappedValue = pastedkey
|
||||
}
|
||||
}
|
||||
TextField("", text: key)
|
||||
.placeholder(when: key.wrappedValue.isEmpty) {
|
||||
Text(title).foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.padding()
|
||||
.padding(.leading, 20)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
}
|
||||
.padding(10)
|
||||
.autocapitalization(.none)
|
||||
.foregroundColor(.white)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.font(.body.monospaced())
|
||||
.textContentType(.password)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray, lineWidth: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label("", systemImage: "doc.on.clipboard")
|
||||
.padding(.leading, 10)
|
||||
.onTapGesture {
|
||||
if let pastedkey = UIPasteboard.general.string {
|
||||
self.key.wrappedValue = pastedkey
|
||||
}
|
||||
struct SignInHeader: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image("logo-nobg")
|
||||
.resizable()
|
||||
.frame(width: 56, height: 56, alignment: .center)
|
||||
.shadow(color: DamusColors.purple, radius: 2)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Sign in", comment: "Title of view to log into an account.")
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("Welcome to the social network you control", comment: "Welcome text")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SignInEntry: View {
|
||||
let key: Binding<String>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
|
||||
.fontWeight(.medium)
|
||||
.padding(.top, 30)
|
||||
|
||||
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateAccountPrompt: View {
|
||||
@Binding var create_account: Bool
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("New to nostr?", comment: "Ask the user if they are new to nostr")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
|
||||
create_account.toggle()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
// let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955"
|
||||
let bech32_pubkey = "KeyInput"
|
||||
Group {
|
||||
LoginView(key: pubkey)
|
||||
LoginView(key: bech32_pubkey)
|
||||
LoginView(key: pubkey, accepted: .constant(true))
|
||||
LoginView(key: bech32_pubkey, accepted: .constant(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ struct TabButton: View {
|
||||
let bits = timeline_to_notification_bits(timeline, ev: nil)
|
||||
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
|
||||
}) {
|
||||
Label("", systemImage: selected == timeline ? "\(img).fill" : img)
|
||||
Image(selected != timeline ? img : "\(img).fill")
|
||||
.contentShape(Rectangle())
|
||||
.frame(maxWidth: .infinity, minHeight: 30.0)
|
||||
}
|
||||
.foregroundColor(selected != timeline ? .gray : .primary)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ struct TabBar: View {
|
||||
VStack {
|
||||
Divider()
|
||||
HStack {
|
||||
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
|
||||
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
|
||||
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
|
||||
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
|
||||
TabButton(timeline: .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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct MutelistView: View {
|
||||
damus_state.postbox.send(new_ev)
|
||||
users = get_mutelist_users(new_ev)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), systemImage: "trash")
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
|
||||
@@ -130,11 +130,11 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if show_images && artifacts.images.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
} else if !show_images && artifacts.images.count > 0 {
|
||||
if show_images && artifacts.media.count > 0 {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
} else if !show_images && artifacts.media.count > 0 {
|
||||
ZStack {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
Blur()
|
||||
.disabled(true)
|
||||
}
|
||||
@@ -261,13 +261,24 @@ struct NoteArtifacts: Equatable {
|
||||
}
|
||||
|
||||
let content: CompatibleText
|
||||
let images: [URL]
|
||||
let urls: [UrlType]
|
||||
let invoices: [Invoice]
|
||||
let links: [URL]
|
||||
|
||||
var media: [MediaUrl] {
|
||||
return urls.compactMap { url in url.is_media }
|
||||
}
|
||||
|
||||
var images: [URL] {
|
||||
return urls.compactMap { url in url.is_img }
|
||||
}
|
||||
|
||||
var links: [URL] {
|
||||
return urls.compactMap { url in url.is_link }
|
||||
}
|
||||
|
||||
static func just_content(_ content: String) -> NoteArtifacts {
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
return NoteArtifacts(content: txt, images: [], invoices: [], links: [])
|
||||
return NoteArtifacts(content: txt, urls: [], invoices: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,8 +315,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
||||
|
||||
func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
var invoices: [Invoice] = []
|
||||
var img_urls: [URL] = []
|
||||
var link_urls: [URL] = []
|
||||
var urls: [UrlType] = []
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({ $0.is_note_mention })
|
||||
@@ -323,12 +333,14 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
var trimmed = txt
|
||||
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, is_image_url(u) {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
@@ -345,25 +357,112 @@ func render_blocks(blocks: [Block], profiles: Profiles) -> NoteArtifacts {
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
case .url(let url):
|
||||
// Handle Image URLs
|
||||
if is_image_url(url) {
|
||||
// Append Image
|
||||
img_urls.append(url)
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
return str
|
||||
} else {
|
||||
link_urls.append(url)
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
||||
return NoteArtifacts(content: txt, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
func is_image_url(_ url: URL) -> Bool {
|
||||
enum MediaUrl {
|
||||
case image(URL)
|
||||
case video(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UrlType {
|
||||
case media(MediaUrl)
|
||||
case link(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_video: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image:
|
||||
return nil
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_img: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video:
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_link: URL? {
|
||||
switch self {
|
||||
case .media:
|
||||
return nil
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_media: MediaUrl? {
|
||||
switch self {
|
||||
case .media(let murl):
|
||||
return murl
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp")
|
||||
return isUrl
|
||||
|
||||
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
|
||||
return .media(.image(url))
|
||||
}
|
||||
|
||||
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") {
|
||||
return .media(.video(url))
|
||||
}
|
||||
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
|
||||
|
||||
@@ -168,7 +168,7 @@ struct EventGroupView: View {
|
||||
func ZapIcon(_ zapgrp: ZapGroup) -> some View {
|
||||
let fmt = format_msats_abbrev(zapgrp.msat_total)
|
||||
return VStack(alignment: .center) {
|
||||
Image(systemName: "bolt.fill")
|
||||
Image("zap.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text(verbatim: fmt)
|
||||
.foregroundColor(Color.orange)
|
||||
@@ -179,13 +179,15 @@ struct EventGroupView: View {
|
||||
Group {
|
||||
switch group {
|
||||
case .repost:
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
Image("repost")
|
||||
.foregroundColor(DamusColors.green)
|
||||
case .reaction:
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("shaka-full")
|
||||
.mask(Image("shaka.fill")
|
||||
.resizable()
|
||||
).frame(width: 24, height: 24)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
)
|
||||
.frame(width: 20, height: 20)
|
||||
case .profile_zap(let zapgrp):
|
||||
ZapIcon(zapgrp)
|
||||
case .zap(let zapgrp):
|
||||
|
||||
@@ -57,7 +57,9 @@ struct ParticipantsView: View {
|
||||
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 {
|
||||
Text(FollowUserView.markdown.process(about))
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
Text(about_string)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
@@ -65,7 +67,7 @@ struct ParticipantsView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
Image("check-circle.fill")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func PostButton(action: @escaping () -> ()) -> some View {
|
||||
radius: 3,
|
||||
x: 3,
|
||||
y: 3)
|
||||
Image(systemName: "plus")
|
||||
Image("plus")
|
||||
.font(.system(.title2))
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ enum NostrPostResult {
|
||||
|
||||
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
|
||||
|
||||
class TagModel: ObservableObject {
|
||||
var diff = 0
|
||||
}
|
||||
|
||||
enum PostAction {
|
||||
case replying_to(NostrEvent)
|
||||
case quoting(NostrEvent)
|
||||
@@ -50,6 +54,7 @@ struct PostView: View {
|
||||
@State var mediaToUpload: MediaUpload? = nil
|
||||
|
||||
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
||||
@StateObject var tagModel: TagModel = TagModel()
|
||||
|
||||
let action: PostAction
|
||||
let damus_state: DamusState
|
||||
@@ -109,7 +114,7 @@ struct PostView: View {
|
||||
Button(action: {
|
||||
attach_media = true
|
||||
}, label: {
|
||||
Image(systemName: "photo")
|
||||
Image("images")
|
||||
.padding(6)
|
||||
})
|
||||
}
|
||||
@@ -118,7 +123,7 @@ struct PostView: View {
|
||||
Button(action: {
|
||||
attach_camera = true
|
||||
}, label: {
|
||||
Image(systemName: "camera")
|
||||
Image("camera")
|
||||
.padding(6)
|
||||
})
|
||||
}
|
||||
@@ -208,6 +213,7 @@ struct PostView: View {
|
||||
focusWordAttributes = (word, range)
|
||||
self.newCursorIndex = nil
|
||||
})
|
||||
.environmentObject(tagModel)
|
||||
.focused($focus)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.onChange(of: post) { p in
|
||||
@@ -338,6 +344,7 @@ struct PostView: View {
|
||||
if let searching {
|
||||
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
|
||||
.frame(maxHeight: .infinity)
|
||||
.environmentObject(tagModel)
|
||||
} else {
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
@@ -464,11 +471,11 @@ struct PVImageCarouselView: View {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = uploadedURL.absoluteString
|
||||
}) {
|
||||
Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), image: "copy")
|
||||
}
|
||||
}
|
||||
}
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
Image("close-circle")
|
||||
.foregroundColor(.white)
|
||||
.padding(20)
|
||||
.shadow(radius: 5)
|
||||
|
||||
@@ -25,6 +25,7 @@ struct UserSearch: View {
|
||||
@Binding var postTextViewCanScroll: Bool
|
||||
|
||||
@Binding var post: NSMutableAttributedString
|
||||
@EnvironmentObject var tagModel: TagModel
|
||||
|
||||
var users: [SearchedUser] {
|
||||
guard let contacts = damus_state.contacts.event else {
|
||||
@@ -48,6 +49,9 @@ struct UserSearch: View {
|
||||
}
|
||||
let mutableString = NSMutableAttributedString(attributedString: post)
|
||||
mutableString.replaceCharacters(in: wordRange, with: tagAttributedString)
|
||||
///adjust cursor position appropriately: ('diff' used in TextViewWrapper / updateUIView after below update of 'post')
|
||||
tagModel.diff = tagAttributedString.length - wordRange.length
|
||||
|
||||
post = mutableString
|
||||
focusWordAttributes = (nil, nil)
|
||||
newCursorIndex = wordRange.location + tagAttributedString.string.count
|
||||
|
||||
@@ -39,7 +39,7 @@ struct EditProfilePictureControl: View {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Image(systemName: "camera")
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 25, height: 25)
|
||||
|
||||
@@ -23,7 +23,8 @@ struct MaybeAnonPfpView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if is_anon {
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
Image("question")
|
||||
.resizable()
|
||||
.font(.largeTitle)
|
||||
.frame(width: size, height: size)
|
||||
} else {
|
||||
|
||||
@@ -10,10 +10,6 @@ import Kingfisher
|
||||
|
||||
let PFP_SIZE: CGFloat = 52.0
|
||||
|
||||
func id_to_color(_ id: String) -> Color {
|
||||
return hex_to_rgb(id)
|
||||
}
|
||||
|
||||
func highlight_color(_ h: Highlight) -> Color {
|
||||
switch h {
|
||||
case .main: return Color.red
|
||||
@@ -42,14 +38,9 @@ struct EditProfilePictureView: View {
|
||||
|
||||
var damus_state: DamusState?
|
||||
|
||||
var PlaceholderColor: Color {
|
||||
return id_to_color(pubkey)
|
||||
}
|
||||
|
||||
var Placeholder: some View {
|
||||
PlaceholderColor
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||
.padding(2)
|
||||
}
|
||||
@@ -98,14 +89,9 @@ struct InnerProfilePicView: View {
|
||||
let highlight: Highlight
|
||||
let disable_animation: Bool
|
||||
|
||||
var PlaceholderColor: Color {
|
||||
return id_to_color(pubkey)
|
||||
}
|
||||
|
||||
var Placeholder: some View {
|
||||
PlaceholderColor
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||
.padding(2)
|
||||
}
|
||||
@@ -196,37 +182,3 @@ struct ProfilePicView_Previews: PreviewProvider {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func hex_to_rgb(_ hex: String) -> Color {
|
||||
guard hex.count >= 6 else {
|
||||
return Color.white
|
||||
}
|
||||
|
||||
let arr = Array(hex.utf8)
|
||||
var rgb: [UInt8] = []
|
||||
var i: Int = arr.count - 12
|
||||
|
||||
while i < arr.count {
|
||||
let cs1 = arr[i]
|
||||
let cs2 = arr[i+1]
|
||||
|
||||
guard let c1 = char_to_hex(cs1) else {
|
||||
return Color.black
|
||||
}
|
||||
|
||||
guard let c2 = char_to_hex(cs2) else {
|
||||
return Color.black
|
||||
}
|
||||
|
||||
rgb.append((c1 << 4) | c2)
|
||||
i += 2
|
||||
}
|
||||
|
||||
return Color.init(
|
||||
.sRGB,
|
||||
red: Double(rgb[0]) / 255,
|
||||
green: Double(rgb[1]) / 255,
|
||||
blue: Double(rgb[2]) / 255,
|
||||
opacity: 1
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ struct ProfileView: View {
|
||||
|
||||
@StateObject var profile: ProfileModel
|
||||
@StateObject var followers: FollowersModel
|
||||
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
|
||||
self.damus_state = damus_state
|
||||
@@ -168,8 +169,8 @@ struct ProfileView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func navImage(systemImage: String) -> some View {
|
||||
Image(systemName: systemImage)
|
||||
func navImage(img: String) -> some View {
|
||||
Image(img)
|
||||
.frame(width: 33, height: 33)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
@@ -179,7 +180,7 @@ struct ProfileView: View {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
navImage(systemImage: "chevron.left")
|
||||
navImage(img: "chevron-left")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +188,7 @@ struct ProfileView: View {
|
||||
Button(action: {
|
||||
action_sheet_presented = true
|
||||
}) {
|
||||
navImage(systemImage: "ellipsis")
|
||||
navImage(img: "share3")
|
||||
}
|
||||
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
@@ -242,15 +243,12 @@ struct ProfileView: View {
|
||||
}
|
||||
|
||||
func lnButton(lnurl: String, profile: Profile) -> some View {
|
||||
let button_img = profile.reactions == false ? "bolt.brakesignal" : "bolt.circle"
|
||||
let button_img = profile.reactions == false ? "zap.fill" : "zap"
|
||||
return Button(action: {
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
|
||||
}
|
||||
zap_button_model.showing_zap_customizer = true
|
||||
}) {
|
||||
Image(systemName: button_img)
|
||||
Image(button_img)
|
||||
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
.contextMenu {
|
||||
if profile.reactions == false {
|
||||
@@ -261,21 +259,50 @@ struct ProfileView: View {
|
||||
Button {
|
||||
UIPasteboard.general.string = addr
|
||||
} label: {
|
||||
Label(addr, systemImage: "doc.on.doc")
|
||||
Label(addr, image: "copy2")
|
||||
}
|
||||
} else if let lnurl = profile.lnurl {
|
||||
Button {
|
||||
UIPasteboard.general.string = lnurl
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.cornerRadius(24)
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +310,7 @@ struct ProfileView: View {
|
||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||
let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
|
||||
return NavigationLink(destination: dmview) {
|
||||
Image(systemName: "bubble.left.circle")
|
||||
Image("messages")
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
}
|
||||
@@ -353,7 +380,9 @@ struct ProfileView: View {
|
||||
var followersCount: some View {
|
||||
HStack {
|
||||
if followers.count == nil {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
Image("download")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
Text("Followers", comment: "Label describing followers of a user.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
@@ -370,9 +399,15 @@ struct ProfileView: View {
|
||||
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
|
||||
|
||||
nameSection(profile_data: profile_data)
|
||||
|
||||
Text(ProfileView.markdown.process(profile_data?.about ?? ""))
|
||||
.font(.subheadline).textSelection(.enabled)
|
||||
|
||||
if let about = profile_data?.about {
|
||||
let blocks = parse_mentions(content: about, tags: [])
|
||||
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
|
||||
SelectableText(attributedString: about_string, size: .subheadline)
|
||||
} else {
|
||||
Text(verbatim: "")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
if let url = profile_data?.website_url {
|
||||
WebsiteLink(url: url)
|
||||
@@ -496,9 +531,6 @@ struct ProfileView_Previews: PreviewProvider {
|
||||
func test_damus_state() -> DamusState {
|
||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let damus = DamusState.empty
|
||||
let settings = UserSettingsStore()
|
||||
settings.donation_percent = 100
|
||||
settings.default_zap_amount = 1971
|
||||
|
||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)
|
||||
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
|
||||
@@ -534,24 +566,12 @@ struct KeyView: View {
|
||||
let bech32 = bech32_pubkey(pubkey) ?? pubkey
|
||||
|
||||
HStack {
|
||||
HStack {
|
||||
Button {
|
||||
copyPubkey(bech32)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
|
||||
.font(.custom("key", size: 12.0))
|
||||
.labelStyle(IconOnlyLabelStyle())
|
||||
.foregroundStyle(hex_to_rgb(pubkey))
|
||||
.symbolRenderingMode(.palette)
|
||||
}
|
||||
.padding(.trailing, 2)
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
|
||||
.font(.footnote)
|
||||
.foregroundColor(keyColor())
|
||||
}
|
||||
.padding(2)
|
||||
.padding([.leading, .trailing], 3)
|
||||
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
|
||||
.font(.footnote)
|
||||
.foregroundColor(keyColor())
|
||||
.padding(5)
|
||||
.padding([.leading, .trailing], 5)
|
||||
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
|
||||
|
||||
if isCopied != true {
|
||||
Button {
|
||||
@@ -560,7 +580,8 @@ struct KeyView: View {
|
||||
Label {
|
||||
Text("Public key", comment: "Label indicating that the text is a user's public account key.")
|
||||
} icon: {
|
||||
Image(systemName: "square.on.square.dashed")
|
||||
Image("copy2")
|
||||
.resizable()
|
||||
.contentShape(Rectangle())
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 20, height: 20)
|
||||
@@ -570,7 +591,8 @@ struct KeyView: View {
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle")
|
||||
Image("check-circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied."))
|
||||
.font(.footnote)
|
||||
|
||||
@@ -30,7 +30,7 @@ struct QRCodeView: View {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
Image("close")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 20)
|
||||
@@ -80,10 +80,17 @@ struct QRCodeView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -52,14 +52,16 @@ struct RecommendedRelayView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "info.circle")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
Image("info")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(Color.accentColor)
|
||||
} else {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "questionmark.circle")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
Image("question")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +87,7 @@ struct RecommendedRelayView: View {
|
||||
Button {
|
||||
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), image: "copy")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +98,9 @@ struct RecommendedRelayView: View {
|
||||
if showText {
|
||||
Text(NSLocalizedString("Connect", comment: "Button to connect to recommended relay server."))
|
||||
}
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
Image("plus-circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ struct RelayStatus: View {
|
||||
if relay.id == self.relay {
|
||||
let c = relay.connection
|
||||
if c.isConnected {
|
||||
conn_image = "network"
|
||||
conn_image = "globe"
|
||||
conn_color = .green
|
||||
} else if c.isConnecting {
|
||||
connecting = true
|
||||
} else {
|
||||
conn_image = "exclamationmark.circle.fill"
|
||||
conn_image = "warning.fill"
|
||||
conn_color = .red
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,14 @@ struct RelayStatus: View {
|
||||
HStack {
|
||||
if connecting {
|
||||
ProgressView()
|
||||
.padding(.trailing, 4)
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing, 5)
|
||||
} else {
|
||||
Image(systemName: conn_image)
|
||||
.frame(width: 8.0, height: 8.0)
|
||||
Image(conn_image)
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(conn_color)
|
||||
.padding(.leading, 5)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
|
||||
@@ -14,7 +14,9 @@ struct RelayType: View {
|
||||
if is_paid {
|
||||
Image("bitcoin-logo")
|
||||
} else {
|
||||
Image(systemName: "globe.americas.fill")
|
||||
Image("globe")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,21 @@ struct RelayView: View {
|
||||
NavigationLink("", destination: RelayDetailView(state: state, relay: relay, nip11: meta)).opacity(0.0)
|
||||
.disabled(showActionButtons)
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "info.circle")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
Image("info")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(Color.accentColor)
|
||||
} else {
|
||||
Text(relay)
|
||||
|
||||
Spacer()
|
||||
Image(systemName: "questionmark.circle")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
|
||||
Image("question")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
@@ -66,7 +71,7 @@ struct RelayView: View {
|
||||
Button {
|
||||
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), systemImage: "doc.on.doc")
|
||||
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), image: "copy2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +92,10 @@ struct RelayView: View {
|
||||
if showText {
|
||||
Text(NSLocalizedString("Disconnect", comment: "Button to disconnect from a relay server."))
|
||||
}
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
|
||||
Image("minus-circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.red)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
|
||||
@@ -23,25 +23,19 @@ struct SaveKeysView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
DamusGradient()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.", comment: "Reminder to user that they should save their account information.")
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Public Key", comment: "Label to indicate that text below is the user's public key used by others to uniquely refer to the user.")
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.")
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
SaveKeyView(text: account.pubkey_bech32, textContentType: .username, is_copied: $pub_copied, focus: $pubkey_focused)
|
||||
@@ -50,11 +44,9 @@ struct SaveKeysView: View {
|
||||
if pub_copied {
|
||||
Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.")
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
SaveKeyView(text: account.privkey_bech32, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
|
||||
@@ -68,18 +60,42 @@ struct SaveKeysView: View {
|
||||
} else if let err = error {
|
||||
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
|
||||
.foregroundColor(.red)
|
||||
DamusWhiteButton(NSLocalizedString("Retry", comment: "Button to retry completing account creation after an error occurred.")) {
|
||||
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
DamusWhiteButton(NSLocalizedString("Let's go!", comment: "Button to complete account creation and start using the app.")) {
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Let's go!", comment: "Button to complete account creation and start using the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.background(
|
||||
Image("eula-bg")
|
||||
.resizable()
|
||||
.blur(radius: 70)
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
.onAppear {
|
||||
@@ -171,8 +187,8 @@ struct SaveKeyView: View {
|
||||
VStack {
|
||||
spacerBlock(width: 0, height: 0)
|
||||
Button(action: copy_text) {
|
||||
Label("", systemImage: is_copied ? "checkmark.circle.fill" : "doc.on.doc")
|
||||
.foregroundColor(is_copied ? .green : .white)
|
||||
Label("", image: is_copied ? "check-circle.fill" : "copy2")
|
||||
.foregroundColor(is_copied ? .green : .gray)
|
||||
.background {
|
||||
if is_copied {
|
||||
Circle()
|
||||
@@ -190,11 +206,10 @@ struct SaveKeyView: View {
|
||||
TextField("", text: .constant(text))
|
||||
.padding(5)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.1)
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
.font(.callout.monospaced())
|
||||
.foregroundColor(.white)
|
||||
.onTapGesture {
|
||||
copy_text()
|
||||
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
|
||||
|
||||
@@ -20,7 +20,7 @@ struct SearchHomeView: View {
|
||||
var SearchInput: some View {
|
||||
HStack {
|
||||
HStack{
|
||||
Image(systemName: "magnifyingglass")
|
||||
Image("search")
|
||||
.foregroundColor(.gray)
|
||||
TextField(NSLocalizedString("Search...", comment: "Placeholder text to prompt entry of search query."), text: $search)
|
||||
.autocorrectionDisabled(true)
|
||||
|
||||
@@ -42,7 +42,7 @@ func describe_search(_ filter: NostrFilter) -> String {
|
||||
struct SearchView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state()
|
||||
let filter = NostrFilter.filter_hashtag(["bitcoin"])
|
||||
let filter = NostrFilter(hashtag: ["bitcoin"])
|
||||
|
||||
let model = SearchModel(state: test_state, search: filter)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ struct SelectWalletView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: self.invoice_copied ? "checkmark.circle" : "doc.on.doc").foregroundColor(.blue)
|
||||
Image(self.invoice_copied ? "check-circle" : "copy2").foregroundColor(.blue)
|
||||
}.clipShape(RoundedRectangle(cornerRadius: 5)).onTapGesture {
|
||||
UIPasteboard.general.string = invoice
|
||||
self.invoice_copied = true
|
||||
|
||||
@@ -14,6 +14,7 @@ struct AppearanceSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
// MARK: - Text Truncation
|
||||
Section(header: Text(NSLocalizedString("Text Truncation", comment: "Section header for damus text truncation user configuration"))) {
|
||||
Toggle(NSLocalizedString("Truncate timeline text", comment: "Setting to truncate text in timeline"), isOn: $settings.truncate_timeline_text)
|
||||
.toggleStyle(.switch)
|
||||
@@ -21,11 +22,13 @@ struct AppearanceSettingsView: View {
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
Section(header: Text(NSLocalizedString("Accessibility", comment: "Section header for accessibility settings"))) {
|
||||
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
// MARK: - Images
|
||||
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
|
||||
Toggle(NSLocalizedString("Animations", comment: "Toggle to enable or disable image animation"), isOn: $settings.enable_animation)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
@@ -66,7 +66,7 @@ struct KeySettingsView: View {
|
||||
}
|
||||
}) {
|
||||
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
|
||||
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
|
||||
Image(copied ? "check-circle" : "copy2")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// SearchSettingsView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Ben Weeks on 29/05/2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchSettingsView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text(NSLocalizedString("Spam", comment: "Section header for Universe/Search spam"))) {
|
||||
Toggle(NSLocalizedString("View multiple events per user", comment: "Setting to only see 1 event per user (npub) in the search/universe"), isOn: $settings.multiple_events_per_pubkey)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Search/Universe", comment: "Navigation title for universe/search settings."))
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchSettingsView(settings: UserSettingsStore())
|
||||
}
|
||||
}
|
||||
@@ -15,71 +15,125 @@ func hex_col(r: UInt8, g: UInt8, b: UInt8) -> Color {
|
||||
opacity: 1.0)
|
||||
}
|
||||
|
||||
enum SetupState {
|
||||
case home
|
||||
case create_account
|
||||
case login
|
||||
}
|
||||
|
||||
|
||||
struct SetupView: View {
|
||||
@State var state: SetupState? = .home
|
||||
@State private var eula = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
DamusGradient()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
NavigationLink(destination: EULAView(state: state), tag: .create_account, selection: $state ) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: EULAView(state: state), tag: .login, selection: $state ) {
|
||||
NavigationLink(destination: EULAView(), isActive: $eula) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("logo-nobg")
|
||||
.resizable()
|
||||
.frame(width: 128.0, height: 128.0, alignment: .center)
|
||||
.padding([.top], 20.0)
|
||||
Text("Damus", comment: "Name of the app, shown on the first screen when user is not logged in.")
|
||||
.font(Font.custom("Nunito", size: 50.0))
|
||||
.kerning(-2)
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: DamusColors.purple, radius: 2)
|
||||
.frame(width: 56, height: 56, alignment: .center)
|
||||
.padding(.top, 20.0)
|
||||
|
||||
CarouselView()
|
||||
|
||||
DamusWhiteButton(NSLocalizedString("Create Account", comment: "Button to create an account.")) {
|
||||
self.state = .create_account
|
||||
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)
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Login", comment: "Button to log into an account.")) {
|
||||
self.state = .login
|
||||
Text("The go-to iOS nostr client", comment: "Quick description of what Damus is")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
.padding(.top, 10)
|
||||
|
||||
WhatIsNostr()
|
||||
.padding()
|
||||
|
||||
WhyWeNeedNostr()
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
eula.toggle()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Let's get started!", comment: "Button to continue to login page.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.padding([.top, .bottom], 20)
|
||||
.foregroundColor(.white)
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Image("login-header")
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: 300, alignment: .center)
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
func DamusWhiteButton(_ title: String, action: @escaping () -> ()) -> some View {
|
||||
return Button(action: action) {
|
||||
Text(title)
|
||||
.frame(width: 300, height: 50)
|
||||
.font(.body.bold())
|
||||
.contentShape(Rectangle())
|
||||
.foregroundColor(.white)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4.0)
|
||||
.stroke(Color.white, lineWidth: 2.0)
|
||||
.background(Color.white.opacity(0.15))
|
||||
)
|
||||
struct LearnAboutNostrLink: View {
|
||||
@Environment(\.openURL) var openURL
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://nostr.com")!)
|
||||
}, label: {
|
||||
Text("Learn more about nostr")
|
||||
.foregroundColor(.accentColor)
|
||||
})
|
||||
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WhatIsNostr: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image("nostr-logo")
|
||||
VStack(alignment: .leading) {
|
||||
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")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
|
||||
LearnAboutNostrLink()
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WhyWeNeedNostr: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image("lightbulb")
|
||||
VStack(alignment: .leading) {
|
||||
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")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SetupView_Previews: PreviewProvider {
|
||||
|
||||