Compare commits

..

1 Commits

151 changed files with 949 additions and 3271 deletions
-2
View File
@@ -1,2 +0,0 @@
translations/
*.lproj/
+5 -105
View File
@@ -1,81 +1,3 @@
## [1.4.1-3] - 2023-04-05
### Added
- Added text truncation settings (William Casarin)
### Changed
- Rename block to mute (William Casarin)
### Fixed
- Reduce chopping of images (mainvolume)
- Fix some notification settings not saving (William Casarin)
- Fix broken camera uploads (again) (Joel Klabo)
[1.4.1-3]: https://github.com/damus-io/damus/releases/tag/v1.4.1-3
## [1.4.1-2] - 2023-04-04
### Added
- Reply counts (William Casarin)
- Add option to only show notification from people you follow (Swift)
- Added local notifications for other events (Swift)
- Show a custom view when tagged user isn't found (ericholguin)
- Show referenced notes in DMs (William Casarin)
### Changed
- Show full bleed images on selected events in threads (William Casarin)
- Improvement to square image displaying (mainvolume)
### Fixed
- Fix broken website links that have missing https:// prefixes (William Casarin)
- Get around CCP bootstrap relay banning by caching user's relays as their bootstrap relays (William Casarin)
[1.4.1-2]: https://github.com/damus-io/damus/releases/tag/v1.4.1-2
## [1.4.1] - 2023-04-03
### Added
- Profile Picture Upload (Joel Klabo)
- Enable offline posting (William Casarin)
- Add auto-translation caching to ruduce api usage (Terry Yiu)
- Added support for gif uploads (Swift)
- Add a Divider in the Follows List for Large Screens (Joel Klabo)
- Upload Photos and Videos from Camera (Joel Klabo)
- Added ability to lookup users by nip05 identifiers (William Casarin)
### Changed
- Only truncate timeline text if enabled in settings (William Casarin)
- Make mentions wide in notifications like in timeline (William Casarin)
- Broadcast events you are replying to (William Casarin)
- Broadcast now also broadcasts event user's profile (William Casarin)
- Improved look of reply view (ericholguin)
- Remove gradient in some places for visibility (ericholguin)
### Fixed
- Fix cropped images (mainvolume)
- Truncate long text in notification items (William Casarin)
- Restore missing reply description on selected events (William Casarin)
- Show sent DMs immediately (William Casarin)
- Fixed size of translated text (William Casarin)
- Fix crash when reposting (William Casarin)
- Fix unclickable image dismiss button (OlegAba)
[1.4.1]: https://github.com/damus-io/damus/releases/tag/v1.4.1
## [1.4.0] - 2023-03-27
### Added
@@ -83,9 +5,6 @@
- Local zap notifications (Swift)
- Add support for video uploads (Swift)
- Auto Translation (Terry Yiu)
- Portuguese (Brazil) translations (Andressa Munturo)
- Spanish (Spain) translations (Max Pleb)
- Vietnamese translations (ShiryoRyo)
### Fixed
@@ -153,10 +72,6 @@
- Add image uploader (Swift)
- Add option to always show images (never blur) (William Casarin)
- Canadian French (Pierre - synoptic_okubo)
- Hungarian translations (Zoltan)
- Korean translations (sogoagain)
- Swedish translations (Pextar)
### Changed
@@ -179,9 +94,6 @@
- Extend user tagging search to all local profiles (William Casarin)
- Vibrate when a zap is received (Swift)
- New and Improved Share sheet (ericholguin)
- Bulgarian translations (elsat)
- Persian translations (Mahdi Taghizadeh)
- Ukrainian translations (Valeriia Khudiakova, Tony B)
### Changed
@@ -280,8 +192,6 @@
- Customized zaps (William Casarin)
- Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo)
- Chinese, Traditional (Hong Kong) translations (rasputin)
- Chinese, Traditional (Taiwan) translations (rasputin)
### Changed
@@ -307,9 +217,6 @@
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
- Czech translations (Martin Gabrhel)
- Indonesian translations (johnybergzy)
- Russian translations (Tony B)
### Changed
@@ -358,6 +265,7 @@
### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin)
@@ -370,10 +278,6 @@
- Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
- Chinese, Simplified (China mainland) translations (haolong, rasputin)
- Dutch translations (Heimen Stoffels - Vistaus)
- Greek translations (milicode)
- Japanese translations (akiomik, foxytanuki, Guetsu Ren - Nighthaven, h3y6e, middlingphys)
### Changed
@@ -408,7 +312,6 @@
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
- Polish translations (pysiak)
### Changed
@@ -431,8 +334,7 @@
### Added
- Arabic translations (Barodane)
- Portuguese translations (Antonio Chagas)
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
@@ -459,8 +361,7 @@
### Added
- Reposts view (Terry Yiu)
- Italian translations (Nicolò Carcagnì)
- Latvian translations (SYX)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
@@ -487,9 +388,7 @@
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- German translations (Gregor, Peter Gerstbach)
- Turkish translations (Taylan Benli)
- French (France) translations (Solobalbo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Add DM Message Requests (William Casarin)
@@ -922,3 +821,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+4 -72
View File
@@ -17,7 +17,6 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -38,13 +37,6 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; };
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; };
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; };
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */; };
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 */; };
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 */; };
@@ -172,7 +164,6 @@
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
@@ -191,8 +182,6 @@
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; };
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -242,7 +231,6 @@
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.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 */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
@@ -264,12 +252,10 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.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 */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */
@@ -324,7 +310,6 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -408,13 +393,6 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; };
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeySettingsView.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@@ -572,7 +550,6 @@
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayBootstrap.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
@@ -591,8 +568,6 @@
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; };
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThiccDivider.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -645,7 +620,6 @@
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.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>"; };
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
@@ -666,12 +640,10 @@
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.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>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -709,7 +681,6 @@
isa = PBXGroup;
children = (
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */,
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */,
);
path = "Empty Views";
sourceTree = "<group>";
@@ -823,23 +794,10 @@
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E23A29D518F000BA313D /* Translations.swift */,
);
path = Models;
sourceTree = "<group>";
};
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
isa = PBXGroup;
children = (
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */,
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */,
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup;
children = (
@@ -863,7 +821,6 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */,
@@ -900,7 +857,6 @@
4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */,
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
F757933929D7AECD007DEAC1 /* ImagePicker.swift */,
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
@@ -951,7 +907,6 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CE4F0F329D779B5005914DB /* PostBox.swift */,
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
@@ -985,7 +940,6 @@
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1028,7 +982,6 @@
children = (
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
@@ -1103,8 +1056,6 @@
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1198,7 +1149,6 @@
children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -1475,7 +1425,6 @@
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
@@ -1486,7 +1435,6 @@
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
@@ -1565,7 +1513,6 @@
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
@@ -1589,11 +1536,9 @@
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -1609,13 +1554,11 @@
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.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 */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
@@ -1656,7 +1599,6 @@
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -1666,11 +1608,9 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
@@ -1681,19 +1621,15 @@
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1983,7 +1919,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1991,9 +1927,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2008,7 +1942,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -2027,7 +1961,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2035,9 +1969,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2052,7 +1984,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
-44
View File
@@ -1,44 +0,0 @@
//
// IconLabel.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
import UIKit
struct IconLabel: View {
let text: String
let img_name: String
let img_color: Color
init(_ text: String, img_name: String, color: Color) {
self.text = text
self.img_name = img_name
self.img_color = color
}
var body: some View {
HStack(spacing: 0) {
Image(systemName: img_name)
.foregroundColor(img_color)
.frame(width: 20)
.padding([.trailing], 20)
Text(text)
}
}}
struct IconLabel_Previews: PreviewProvider {
static var previews: some View {
Form {
Section {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange)
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
}
}
}
}
+21 -116
View File
@@ -32,42 +32,12 @@ struct ShareSheet: UIViewControllerRepresentable {
}
enum ImageShape {
case square
case landscape
case portrait
case unknown
}
struct ImageCarousel: View {
var urls: [URL]
let evid: String
let previews: PreviewCache
@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 * 0.85
init(previews: PreviewCache, evid: String, urls: [URL]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
self.urls = urls
self.evid = evid
self.previews = previews
}
var filling: Bool {
image_fill?.filling == true
}
var height: CGFloat {
image_fill?.height ?? 0
}
@State var open_sheet: Bool = false
@State var current_url: URL? = nil
var body: some View {
TabView {
@@ -75,32 +45,30 @@ struct ImageCarousel: View {
Rectangle()
.foregroundColor(Color.clear)
.overlay {
GeometryReader { geo in
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
previews.cache_image_meta(evid: evid, image_fill: fill)
image_fill = fill
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
}
KFAnimatedImage(url)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fill)
//.cornerRadius(10)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
// .contextMenu {
// Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
// UIPasteboard.general.string = url.absoluteString
// }
// }
}
}
}
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: height)
.frame(height: 350)
.onTapGesture {
open_sheet = true
}
@@ -108,71 +76,8 @@ struct ImageCarousel: View {
}
}
// MARK: - Image Modifier
extension KFOptionSetter {
/// Sets a block to get image size
///
/// - Parameter block: The block which is used to read the image object.
/// - Returns: `Self` value after read size
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
let img_size = image.size
let geo_size = size
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
img_size: img_size,
maxHeight: max,
fillHeight: fill)
DispatchQueue.main.async { [block, fill] in
try? block(fill)
}
return image
}
options.imageModifier = modifier
return self
}
}
public struct ImageFill {
let filling: Bool?
let height: CGFloat
static func determine_image_shape(_ size: CGSize) -> ImageShape {
guard size.height > 0 else {
return .unknown
}
let imageRatio = size.width / size.height
switch imageRatio {
case 1.0: return .square
case ..<1.0: return .portrait
case 1.0...: return .landscape
default: return .unknown
}
}
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
let shape = determine_image_shape(img_size)
let xfactor = geo_size.width / img_size.width
let scaled = img_size.height * xfactor
// calculate scaled image height
// set scale factor and constrain images to minimum 150
// and animations to scaled factor for dynamic size adjustment
switch shape {
case .portrait, .landscape:
let filling = scaled > maxHeight
let height = filling ? fillHeight : scaled
return ImageFill(filling: filling, height: height)
case .square, .unknown:
return ImageFill(filling: nil, height: scaled)
}
}
}
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}
+1 -3
View File
@@ -15,14 +15,12 @@ struct SelectableText: View {
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: eventviewsize_to_uifont(size),
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
-22
View File
@@ -1,22 +0,0 @@
//
// ThiccDivider.swift
// damus
//
// Created by William Casarin on 2023-04-03.
//
import SwiftUI
struct ThiccDivider: View {
var body: some View {
Rectangle()
.frame(height: 4)
.foregroundColor(DamusColors.adaptableGrey)
}
}
struct ThiccDivider_Previews: PreviewProvider {
static var previews: some View {
ThiccDivider()
}
}
+73 -74
View File
@@ -11,74 +11,22 @@ import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true
@State var noteLanguage: String?
@State var show_translated_note: Bool
@State var translated_artifacts: NoteArtifacts?
@State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
@State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
} else {
self._translated_artifacts = State(initialValue: nil)
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
}
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
processTranslation()
}
.translate_button_style()
}
func processTranslation() {
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
return
}
checkingTranslationStatus = true
show_translated_note = true
Task {
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
DispatchQueue.main.async {
guard translationWithLanguage != nil else {
noteLanguage = damus_state.translations.targetLanguage
checkingTranslationStatus = false
show_translated_note = false
translatable = false
return
}
noteLanguage = translationWithLanguage!.language
// Render translated note.
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
translatable = true
checkingTranslationStatus = false
}
}
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
@@ -87,40 +35,91 @@ struct TranslateView: View {
}
.translate_button_style()
SelectableText(attributedString: artifacts.content, size: self.size)
SelectableText(attributedString: artifacts.content)
}
}
func CheckingStatus(lang: String) -> some View {
return Button(String(format: NSLocalizedString("Translating from %@...", comment: "Button to indicate that the note is in the process of being translated from a different language."), lang)) {
show_translated_note = false
}
.translate_button_style()
}
func MainContent(note_lang: String) -> some View {
return Group {
if translatable {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
EmptyView()
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let lang = languageName, show_translated_note {
if checkingTranslationStatus {
CheckingStatus(lang: lang)
} else if let artifacts = translated_artifacts {
Translated(lang: lang, artifacts: artifacts)
}
} else {
EmptyView()
TranslateButton
}
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else {
Text("")
}
}
.task {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
checkingTranslationStatus = true
if #available(iOS 16, *) {
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
currentLanguage = Locale.current.languageCode ?? "en"
}
noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
translated_note = nil
checkingTranslationStatus = false
return
}
if !preferredLanguages.contains(note_lang) {
do {
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
if originalContent == translated_note {
// If the translation is the same as the original, don't bother showing it.
noteLanguage = currentLanguage
translated_note = nil
}
} catch {
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
noteLanguage = currentLanguage
translated_note = nil
}
}
if let translated = translated_note {
// Render translated note.
let translatedBlocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
show_translated_note = damus_state.settings.auto_translate
}
}
}
@@ -136,6 +135,6 @@ extension View {
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .normal)
TranslateView(damus_state: ds, event: test_event)
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ struct WebsiteLink: View {
}, label: {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
.foregroundStyle(LINEAR_GRADIENT)
})
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ struct ZapButton: View {
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
}
}
+66 -72
View File
@@ -8,10 +8,16 @@
import SwiftUI
import Starscream
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
struct TimestampedProfile {
let profile: Profile
let timestamp: Int64
let event: NostrEvent
}
enum Sheets: Identifiable {
@@ -76,9 +82,9 @@ struct ContentView: View {
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var muting: String? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var blocking: String? = nil
@State var confirm_block: Bool = false
@State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies
@@ -141,8 +147,22 @@ struct ContentView: View {
}
var timelineNavItem: Text {
return Text(timeline_name(selected_timeline))
.bold()
switch selected_timeline {
case .home:
return Text("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
.bold()
case .dms:
return Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
return Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
return Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
return Text(verbatim: "")
}
}
func MainContent(damus: DamusState) -> some View {
@@ -228,9 +248,9 @@ struct ContentView: View {
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
if let sec = damus_state.keypair.privkey {
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
if let ds = damus_state {
if let sec = ds.keypair.privkey {
ReportView(pool: ds.pool, target: target, privkey: sec)
} else {
EmptyView()
}
@@ -306,9 +326,9 @@ struct ContentView: View {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, damus_state: damus_state!)
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
ReplyView(replying_to: event, damus: damus_state!)
case .event:
EventDetailView()
case .filter:
@@ -369,20 +389,14 @@ struct ContentView: View {
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
}
.onReceive(handle_notify(.mute)) { notif in
.onReceive(handle_notify(.block)) { notif in
let pubkey = notif.object as! String
self.muting = pubkey
self.confirm_mute = true
self.blocking = pubkey
self.confirm_block = true
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
guard let ds = self.damus_state else {
return
}
ds.postbox.send(ev)
if let profile = ds.profiles.profiles[ev.pubkey] {
ds.postbox.send(profile.event)
}
self.damus_state?.pool.send(.event(ev))
}
.onReceive(handle_notify(.unfollow)) { notif in
guard let privkey = self.privkey else {
@@ -396,7 +410,7 @@ struct ContentView: View {
let target = notif.object as! FollowTarget
let pk = target.pubkey
if let ev = unfollow_user(postbox: damus.postbox,
if let ev = unfollow_user(pool: damus.pool,
our_contacts: damus.contacts.event,
pubkey: damus.pubkey,
privkey: privkey,
@@ -447,16 +461,7 @@ struct ContentView: View {
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
guard let ds = self.damus_state else {
return
}
ds.postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events
if let ev = ds.events.lookup(eref.ref_id) {
ds.postbox.send(ev)
}
}
self.damus_state?.pool.send(.event(new_ev))
case .cancel:
active_sheet = nil
print("post cancelled")
@@ -474,23 +479,23 @@ struct ContentView: View {
notify(.logout, ())
}
}
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
user_muted_confirm = false
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
}, message: {
if let pubkey = self.muting {
if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false
confirm_mute = false
confirm_block = false
}
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
@@ -502,7 +507,7 @@ struct ContentView: View {
return
}
guard let pubkey = muting else {
guard let pubkey = blocking else {
return
}
@@ -511,20 +516,20 @@ struct ContentView: View {
}
damus_state?.contacts.set_mutelist(mutelist)
ds.postbox.send(mutelist)
ds.pool.send(.event(mutelist))
confirm_overwrite_mutelist = false
confirm_mute = false
user_muted_confirm = true
confirm_block = false
user_blocked_confirm = true
}
}, message: {
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
})
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
confirm_mute = false
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
confirm_block = false
}
Button(NSLocalizedString("Mute", comment: "Alert button to mute a user."), role: .destructive) {
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
guard let ds = damus_state else {
return
}
@@ -535,7 +540,7 @@ struct ContentView: View {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
guard let pubkey = blocking else {
return
}
@@ -543,16 +548,16 @@ struct ContentView: View {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev)
ds.pool.send(.event(ev))
}
}
}, message: {
if let pubkey = muting {
if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
} else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
}
})
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
@@ -560,9 +565,7 @@ struct ContentView: View {
current_boost = nil
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
}
self.damus_state?.pool.send(.event(current_boost!))
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
@@ -598,10 +601,9 @@ struct ContentView: View {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
for relay in BOOTSTRAP_RELAYS {
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
@@ -609,8 +611,6 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
let settings = UserSettingsStore()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
@@ -622,16 +622,12 @@ struct ContentView: View {
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: settings,
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
bookmarks: BookmarksManager(pubkey: pubkey)
)
home.damus_state = self.damus_state!
@@ -809,8 +805,6 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
}
switch ev {
case .ok:
break
case .event(_, let ev):
has_event = true
callback(ev)
@@ -837,12 +831,12 @@ func timeline_name(_ timeline: Timeline?) -> String {
}
switch timeline {
case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
return "Home"
case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
return "Notifications"
case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
return "Universe 🛸"
case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
return "DMs"
}
}
+2 -12
View File
@@ -11,40 +11,34 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zap?
@Published var likes: Int
@Published var boosts: Int
@Published var zaps: Int
@Published var zap_total: Int64
@Published var replies: Int
static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
self.replies = replies
self.zap_total = zap_total
self.our_like = our_like
self.our_boost = our_boost
self.our_zap = our_zap
self.our_reply = our_reply
}
func update(damus: DamusState, evid: String) {
self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0
self.replies = damus.replies.get_replies(evid)
self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid)
self.objectWillChange.send()
}
@@ -60,10 +54,6 @@ class ActionBarModel: ObservableObject {
return our_like != nil
}
var replied: Bool {
return our_reply != nil
}
var boosted: Bool {
return our_boost != nil
}
+2 -2
View File
@@ -140,7 +140,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri
return ev
}
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
guard let cs = our_contacts else {
return nil
}
@@ -149,7 +149,7 @@ func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String,
ev.calculate_id()
ev.sign(privkey: privkey)
postbox.send(ev)
pool.send(.event(ev))
return ev
}
-1
View File
@@ -14,7 +14,6 @@ class CreateAccountModel: ObservableObject {
@Published var about: String = ""
@Published var pubkey: String = ""
@Published var privkey: String = ""
@Published var profile_image: String? = nil
var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? ""
+2 -6
View File
@@ -26,10 +26,7 @@ struct DamusState {
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
var pubkey: String {
return keypair.pubkey
}
@@ -39,7 +36,6 @@ struct DamusState {
}
static var empty: DamusState {
let settings = UserSettingsStore()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""))
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
//
// DraftModel.swift
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
-2
View File
@@ -64,8 +64,6 @@ class EventsModel: ObservableObject {
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .ok:
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
-3
View File
@@ -94,9 +94,6 @@ class FollowersModel: ObservableObject {
} else if sub_id == self.profiles_id {
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
}
case .ok:
break
}
}
}
-2
View File
@@ -58,8 +58,6 @@ class FollowingModel {
break
case .nostr_event(let nev):
switch nev {
case .ok:
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
+22 -118
View File
@@ -150,10 +150,8 @@ class HomeModel: ObservableObject {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
}
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
}
return
@@ -335,11 +333,7 @@ class HomeModel: ObservableObject {
self.loading = false
break
case .ok:
break
}
}
}
@@ -465,7 +459,7 @@ class HomeModel: ObservableObject {
return m[kind]
}
func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else {
@@ -489,10 +483,7 @@ class HomeModel: ObservableObject {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
process_local_notification(damus_state: damus_state, event: ev)
}
handle_last_event(ev: ev, timeline: .notifications)
}
@discardableResult
@@ -511,13 +502,11 @@ class HomeModel: ObservableObject {
}
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.replies.count_replies(ev)
damus_state.events.insert(ev)
if sub_id == home_subid {
@@ -543,13 +532,9 @@ class HomeModel: ObservableObject {
incoming_dms.append(ev)
dm_debouncer.debounce { [self] in
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
}
self.incoming_dms = []
}
@@ -678,7 +663,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
}
}
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
@@ -744,7 +729,7 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
}
@@ -779,7 +764,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
}
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
notify(.relays_changed, ())
}
}
@@ -949,37 +933,30 @@ func zap_vibrate(zap_amount: Int64) {
vibration_generator.impactOccurred()
}
func zap_notification_title(_ zap: Zap) -> String {
func describe_zap_type(_ zap: Zap) -> String? {
if zap.private_request != nil {
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
} else {
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
return "Private"
}
return nil
}
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
func create_in_app_zap_notification(profiles: Profiles, zap: Zap) {
let content = UNMutableNotificationContent()
let typ = describe_zap_type(zap).map({ "\($0) " }) ?? ""
content.title = typ + "Zap"
let satString = zap.invoice.amount == 1000 ? "sat" : "sats"
let src = zap.private_request ?? zap.request.ev
let anon = event_is_anonymous(ev: src)
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount)
let sats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
} else {
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
let message = src.content.count == 0 ? "" : ": \"\(src.content)\""
content.body = "You received \(sats) \(satString) from \(name)\(message)"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -995,76 +972,3 @@ func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale
}
}
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
guard let type = ev.known_kind else {
return
}
if damus_state.settings.notification_only_from_following,
damus_state.contacts.follow_state(ev.pubkey) != .follows
{
return
}
if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) {
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
}
} else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
let e_ref = ev.referenced_ids.first?.ref_id,
let content = damus_state.events.lookup(e_ref)?.content {
create_local_notification(displayName: displayName, conversation: content, type: type)
}
}
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
let content = UNMutableNotificationContent()
var title = ""
var identifier = ""
switch type {
case .text:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification"
case .boost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification"
case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification"
default:
break
}
content.title = title
content.body = conversation
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
-2
View File
@@ -133,8 +133,6 @@ class ProfileModel: ObservableObject, Equatable {
return
}
switch resp {
case .ok:
break
case .event(_, let ev):
add_event(ev)
case .notice(let notice):
-2
View File
@@ -68,8 +68,6 @@ class SearchHomeModel: ObservableObject {
}
case .notice(let msg):
print("search home notice: \(msg)")
case .ok:
break
case .eose(let sub_id):
loading = false
-3
View File
@@ -131,9 +131,6 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
case .event(let ev_subid, let ev):
handle(ev_subid, ev)
return (ev_subid, false)
case .ok:
return (nil, false)
case .notice(let note):
if note.contains("Too many subscription filters") {
-1
View File
@@ -114,7 +114,6 @@ class ThreadModel: ObservableObject {
}
let the_ev = damus_state.events.upsert(ev)
damus_state.replies.count_replies(the_ev)
damus_state.events.add_replies(ev: the_ev)
event_map.insert(ev)
-150
View File
@@ -1,150 +0,0 @@
//
// Translations.swift
// damus
//
// Created by Terry Yiu on 3/29/23.
//
import Foundation
import NaturalLanguage
class Translations: ObservableObject {
private static let languageDetectionMinConfidence = 0.5
@Published var translations: [NostrEvent: String] = [:]
@Published var languages: [NostrEvent: String] = [:]
let settings: UserSettingsStore
let translator: Translator
let targetLanguage = currentLanguage()
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(_ settings: UserSettingsStore) {
self.settings = settings
self.translator = Translator(settings)
}
/**
Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
The detected language will be returned only if it has a 50% or more confidence.
This is a best effort guess and could be incorrect.
*/
func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
if let cachedLanguage = languages[event] {
return cachedLanguage
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
let language = localeToLanguage(locale)
languages[event] = language
return language
}
/**
Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines.
*/
private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool {
return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines)
}
func hasCachedTranslation(_ event: NostrEvent) -> Bool {
return languages[event] != nil
}
func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? {
if let cachedLanguage = languages[event] {
if let cachedTranslation = translations[event] {
return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage)
} else {
return nil
}
} else {
return nil
}
}
func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
guard shouldTranslate(event, state: state) else {
return nil
}
guard let noteLanguage = detectLanguage(event, state: state) else {
return nil
}
if languages[event] != nil {
return cachedTranslation(event)
}
do {
guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
return nil
}
// If the translated content is identical to the original content, don't return the translation.
if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) {
// Nil out the translation as it's the same as the original.
translations[event] = nil
// Leave an entry so that we don't attempt to translate it again in the future.
languages[event] = targetLanguage
return nil
} else {
translations[event] = translationWithLanguage.translation
languages[event] = translationWithLanguage.language
return translationWithLanguage
}
} catch {
return nil
}
}
func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
// Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
// it's annoying and unexpected for the translation to show up.
if event.pubkey == state.pubkey && state.is_privkey_user {
return false
}
// Avoid translating if no translation service is configured.
switch settings.translation_service {
case .none:
return false
case .libretranslate:
if URLComponents(string: settings.libretranslate_url) == nil {
return false
}
case .deepl:
if settings.deepl_api_key == "" {
return false
}
}
// If translation was attempted before, use the results of the cached translation to determine if it should be shown.
if languages[event] != nil {
return translations[event] != nil
}
// Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
return false
}
return true
}
}
+11 -56
View File
@@ -128,54 +128,6 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var zap_notification: Bool {
didSet {
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
}
}
@Published var mention_notification: Bool {
didSet {
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
}
}
@Published var repost_notification: Bool {
didSet {
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
}
}
@Published var dm_notification: Bool {
didSet {
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
}
}
@Published var like_notification: Bool {
didSet {
UserDefaults.standard.set(like_notification, forKey: "like_notification")
}
}
@Published var notification_only_from_following: Bool {
didSet {
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
}
}
@Published var truncate_timeline_text: Bool {
didSet {
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
}
}
@Published var truncate_mention_text: Bool {
didSet {
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
}
}
@Published var auto_translate: Bool {
didSet {
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
@@ -269,14 +221,6 @@ class UserSettingsStore: ObservableObject {
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
disable_animation = should_disable_image_animation()
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
@@ -336,6 +280,17 @@ class UserSettingsStore: ObservableObject {
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
+13 -13
View File
@@ -42,42 +42,42 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: -1, tag: "systemdefaultwallet", displayName: NSLocalizedString("Local default", comment: "Dropdown option label for system default for Lightning wallet."),
link: "lightning:", appStoreLink: "lightning:", image: "")
case .strike:
return .init(index: 0, tag: "strike", displayName: "Strike", link: "strike:",
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
case .cashapp:
return .init(index: 1, tag: "cashapp", displayName: "Cash App", link: "https://cash.app/launch/lightning/",
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
case .muun:
return .init(index: 2, tag: "muun", displayName: "Muun", link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
case .bluewallet:
return .init(index: 3, tag: "bluewallet", displayName: "Blue Wallet", link: "bluewallet:lightning:",
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi:
return .init(index: 4, tag: "walletofsatoshi", displayName: "Wallet of Satoshi", link: "walletofsatoshi:lightning:",
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee:
return .init(index: 5, tag: "zebedee", displayName: "Zebedee", link: "zebedee:lightning:",
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
appStoreLink: "https://apps.apple.com/us/app/zebedee-wallet/id1484394401", image: "zebedee")
case .zeusln:
return .init(index: 6, tag: "zeusln", displayName: "Zeus LN", link: "zeusln:lightning:",
return .init(index: 6, tag: "zeusln", displayName: NSLocalizedString("Zeus LN", comment: "Dropdown option label for Lightning wallet, Zeus LN."), link: "zeusln:lightning:",
appStoreLink: "https://apps.apple.com/us/app/zeus-ln/id1456038895", image: "zeusln")
case .lnlink:
return .init(index: 7, tag: "lnlink", displayName: "LNLink", link: "lnlink:lightning:",
return .init(index: 7, tag: "lnlink", displayName: NSLocalizedString("LNLink", comment: "Dropdown option label for Lightning wallet, LNLink."), link: "lnlink:lightning:",
appStoreLink: "https://testflight.apple.com/join/aNY4yuuZ", image: "lnlink")
case .phoenix:
return .init(index: 8, tag: "phoenix", displayName: "Phoenix", link: "phoenix://",
return .init(index: 8, tag: "phoenix", displayName: NSLocalizedString("Phoenix", comment: "Dropdown option label for Lightning wallet, Phoenix."), link: "phoenix://",
appStoreLink: "https://apps.apple.com/us/app/phoenix-wallet/id1544097028", image: "phoenix")
case .breez:
return .init(index: 9, tag: "breez", displayName: "Breez", link: "breez:",
return .init(index: 9, tag: "breez", displayName: NSLocalizedString("Breez", comment: "Dropdown option label for Lightning wallet, Breez."), link: "breez:",
appStoreLink: "https://apps.apple.com/us/app/breez-lightning-client-pos/id1463604142", image: "breez")
case .bitcoinbeach:
return .init(index: 10, tag: "bitcoinbeach", displayName: "Bitcoin Beach", link: "bitcoinbeach://",
return .init(index: 10, tag: "bitcoinbeach", displayName: NSLocalizedString("Bitcoin Beach", comment: "Dropdown option label for Lightning wallet, Bitcoin Beach."), link: "bitcoinbeach://",
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
case .blixtwallet:
return .init(index: 11, tag: "blixtwallet", displayName: "Blixt Wallet", link: "blixtwallet:lightning:",
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
case .river:
return .init(index: 12, tag: "river", displayName: "River", link: "river://",
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "river://",
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
}
-2
View File
@@ -46,8 +46,6 @@ class ZapsModel: ObservableObject {
}
switch resp {
case .ok:
break
case .notice:
break
case .eose:
+1 -10
View File
@@ -98,16 +98,7 @@ struct Profile: Codable {
}
var website_url: URL? {
if self.website?.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
return nil
}
return self.website.flatMap { url in
let trim = url.trimmingCharacters(in: .whitespacesAndNewlines)
if !(trim.hasPrefix("http://") || trim.hasPrefix("https://")) {
return URL(string: "https://" + trim)
}
return URL(string: trim)
}
return self.website.flatMap { URL(string: $0) }
}
var lnurl: String? {
+21 -7
View File
@@ -260,6 +260,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return event_is_reply(self, privkey: privkey)
}
func note_language(_ privkey: String?) -> String? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = blocks(privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
return localeToLanguage(locale)
}
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
@@ -519,21 +538,16 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
guard let privkey = keypair.privkey else {
return nil
}
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:]
for relay in bootstrap_relays {
for relay in BOOTSTRAP_RELAYS {
relays[relay] = rw_relay_info
}
let relay_json = encode_json(relays)!
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
let tags = [
["p", damus_pubkey],
["p", jb55_pubkey],
["p", keypair.pubkey] // you're a friend of yourself!
]
let ev = NostrEvent(content: relay_json,
+1 -1
View File
@@ -21,5 +21,5 @@ struct NostrMetadata: Codable {
}
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, banner: nil, lud06: nil, lud16: nil)
}
+1 -24
View File
@@ -7,22 +7,13 @@
import Foundation
struct CommandResult {
let event_id: String
let ok: Bool
let msg: String
}
enum NostrResponse: Decodable {
case event(String, NostrEvent)
case notice(String)
case eose(String)
case ok(CommandResult)
var subid: String? {
switch self {
case .ok(_):
return nil
case .event(let sub_id, _):
return sub_id
case .eose(let sub_id):
@@ -57,23 +48,9 @@ enum NostrResponse: Decodable {
let sub_id = try container.decode(String.self)
self = .eose(sub_id)
return
} else if typ == "OK" {
var cr: CommandResult
do {
let event_id = try container.decode(String.self)
let ok = try container.decode(Bool.self)
let msg = try container.decode(String.self)
cr = CommandResult(event_id: event_id, ok: ok, msg: msg)
} catch {
print(error)
throw error
}
self = .ok(cr)
return
//ev.pow = count_hash_leading_zero_bits(ev.id)
}
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT, NOTICE or OK, got \(typ)"))
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)"))
}
}
+1
View File
@@ -256,6 +256,7 @@ class RelayPool {
}
}
// handle reconnect logic, etc?
for handler in handlers {
handler.callback(relay_id, event)
}
+1 -1
View File
@@ -39,7 +39,7 @@ enum DisplayName {
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
if pubkey == "anon" {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
return .one("Anonymous")
}
guard let profile else {
-8
View File
@@ -22,14 +22,6 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
func currentLanguage() -> String {
if #available(iOS 16, *) {
return Locale.current.language.languageCode?.identifier ?? "en"
} else {
return Locale.current.languageCode ?? "en"
}
}
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
+2 -2
View File
@@ -86,8 +86,8 @@ extension Notification.Name {
static var report: Notification.Name {
return Notification.Name("report")
}
static var mute: Notification.Name {
return Notification.Name("mute")
static var block: Notification.Name {
return Notification.Name("block")
}
static var new_mutes: Notification.Name {
return Notification.Name("new_mutes")
-113
View File
@@ -6,116 +6,3 @@
//
import Foundation
class Relayer {
let relay: String
var attempts: Int
var retry_after: Double
var last_attempt: Int64?
init(relay: String, attempts: Int, retry_after: Double) {
self.relay = relay
self.attempts = attempts
self.retry_after = retry_after
self.last_attempt = nil
}
}
class PostedEvent {
let event: NostrEvent
var remaining: [Relayer]
init(event: NostrEvent, remaining: [String]) {
self.event = event
self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
}
}
}
class PostBox {
let pool: RelayPool
var events: [String: PostedEvent]
init(pool: RelayPool) {
self.pool = pool
self.events = [:]
pool.register_handler(sub_id: "postbox", handler: handle_event)
}
func try_flushing_events() {
let now = Int64(Date().timeIntervalSince1970)
for kv in events {
let event = kv.value
for relayer in event.remaining {
if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
flush_event(event, to_relay: relayer)
}
}
}
}
func handle_event(relay_id: String, _ ev: NostrConnectionEvent) {
try_flushing_events()
guard case .nostr_event(let resp) = ev else {
return
}
guard case .ok(let cr) = resp else {
return
}
remove_relayer(relay_id: relay_id, event_id: cr.event_id)
}
func remove_relayer(relay_id: String, event_id: String) {
guard let ev = self.events[event_id] else {
return
}
ev.remaining = ev.remaining.filter {
$0.relay != relay_id
}
if ev.remaining.count == 0 {
self.events.removeValue(forKey: event_id)
}
}
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
var relayers = event.remaining
if let to_relay {
relayers = [to_relay]
}
for relayer in relayers {
relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5
pool.send(.event(event.event), to: [relayer.relay])
}
}
func flush() {
for event in events {
flush_event(event.value)
}
}
func send(_ event: NostrEvent) {
// Don't add event if we already have it
if events[event.id] != nil {
return
}
let remaining = pool.descriptors.map {
$0.url.absoluteString
}
let posted_ev = PostedEvent(event: event, remaining: remaining)
events[event.id] = posted_ev
flush_event(posted_ev)
}
}
+1 -11
View File
@@ -24,21 +24,12 @@ enum Preview {
}
class PreviewCache {
private var previews: [String: Preview]
private var image_meta: [String: ImageFill]
var previews: [String: Preview]
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
}
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
@@ -50,6 +41,5 @@ class PreviewCache {
init() {
self.previews = [:]
self.image_meta = [:]
}
}
-44
View File
@@ -1,44 +0,0 @@
//
// RelayBootstrap.swift
// damus
//
// Created by William Casarin on 2023-04-04.
//
import Foundation
let BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
func bootstrap_relays_setting_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "bootstrap_relays")
}
func save_bootstrap_relays(pubkey: String, relays: [String]) {
let key = bootstrap_relays_setting_key(pubkey: pubkey)
UserDefaults.standard.set(relays, forKey: key)
}
func load_bootstrap_relays(pubkey: String) -> [String] {
let key = bootstrap_relays_setting_key(pubkey: pubkey)
guard let relays = UserDefaults.standard.stringArray(forKey: key) else {
print("loading default bootstrap relays")
return BOOTSTRAP_RELAYS.map { $0 }
}
if relays.count == 0 {
print("loading default bootstrap relays")
return BOOTSTRAP_RELAYS.map { $0 }
}
let loaded_relays = Array(Set(relays + BOOTSTRAP_RELAYS))
print("Loading custom bootstrap relays: \(loaded_relays)")
return loaded_relays
}
-54
View File
@@ -1,54 +0,0 @@
//
// ReplyCounter.swift
// damus
//
// Created by William Casarin on 2023-04-04.
//
import Foundation
class ReplyCounter {
private var replies: [String: Int]
private var counted: Set<String>
private var our_replies: [String: NostrEvent]
private let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
replies = [:]
counted = Set()
our_replies = [:]
}
func our_reply(_ evid: String) -> NostrEvent? {
return our_replies[evid]
}
func get_replies(_ evid: String) -> Int {
return replies[evid] ?? 0
}
func count_replies(_ event: NostrEvent) {
guard event.is_textlike else {
return
}
if counted.contains(event.id) {
return
}
counted.insert(event.id)
for reply in event.direct_replies(nil) {
if event.pubkey == our_pubkey {
self.our_replies[reply.ref_id] = event
}
if replies[reply.ref_id] != nil {
replies[reply.ref_id] = replies[reply.ref_id]! + 1
} else {
replies[reply.ref_id] = 1
}
}
}
}
+9 -34
View File
@@ -20,28 +20,18 @@ public struct Translator {
self.userSettingsStore = userSettingsStore
}
/**
Translates a string from source language to target language.
If the translation provider supports its own language detection, it may determine the source language by itself that could be
different from what is passed in as the sourceLanguage argument.
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
If the translation was unable to be fetched for whatever reason, nil is returned.
*/
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, to: targetLanguage)
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return nil
return text
}
}
/**
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
*/
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
@@ -61,15 +51,10 @@ public struct Translator {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
let translation = response.translatedText
return TranslationWithLanguage(translation: translation, language: targetLanguage)
return response.translatedText
}
/**
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
*/
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
@@ -83,9 +68,10 @@ public struct Translator {
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
@@ -97,13 +83,7 @@ public struct Translator {
}
let response: Response = try await decodedData(for: request)
if response.translations.isEmpty {
return nil
}
let translation = response.translations.map { $0.text }.joined(separator: " ")
return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
@@ -124,11 +104,6 @@ public struct Translator {
}
}
public struct TranslationWithLanguage {
let translation: String
let language: String
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
+10 -15
View File
@@ -47,15 +47,10 @@ struct EventActionBar: View {
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? Color.blue : Color.gray)
EventActionButton(img: "bubble.left", col: nil) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
}
Spacer()
HStack(spacing: 4) {
@@ -159,7 +154,7 @@ struct EventActionBar: View {
generator.impactOccurred()
damus_state.postbox.send(like_ev)
damus_state.pool.send(.event(like_ev))
}
}
@@ -230,12 +225,12 @@ struct EventActionBar_Previews: PreviewProvider {
let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel.empty()
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar)
+77 -2
View File
@@ -40,9 +40,9 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
switch mediaToUpload {
case .image(let url):
case .image(let img):
do {
mediaData = try Data(contentsOf: url)
mediaData = try Data(contentsOf: img)
} catch {
return .failed(error)
}
@@ -80,6 +80,81 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
}
}
extension PostView {
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let damusState: DamusState
let onImagePicked: (URL) -> Void
let onVideoPicked: (URL) -> Void
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (URL) -> Void
private let onVideoPicked: (URL) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (URL) -> Void,
onVideoPicked: @escaping (URL) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
self.onVideoPicked = onVideoPicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
// Handle the selected video
onVideoPicked(videoURL)
} else if let imageURL = info[UIImagePickerController.InfoKey.imageURL] as? URL {
// Handle the selected image
self.onImagePicked(imageURL)
}
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: { url in
// Handle the selected image URL
onImagePicked(url)
},
onVideoPicked: { videoURL in
// Handle the selected video URL
onVideoPicked(videoURL)
})
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
let mediaUploader = get_media_uploader(damusState.keypair.pubkey)
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
if mediaUploader.supportsVideo {
picker.mediaTypes.append("public.movie")
}
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
}
extension NSMutableData {
func appendString(string: String) {
guard let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true) else {
+272 -18
View File
@@ -17,14 +17,26 @@ struct ConfigView: View {
@State var confirm_logout: Bool = false
@State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false
@State var show_privkey: Bool = false
@State var has_authenticated_locally: Bool = false
@State var show_api_key: Bool = false
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var delete_text: String = ""
@State var default_zap_amount: String
@ObservedObject var settings: UserSettingsStore
let generator = UIImpactFeedbackGenerator(style: .light)
private let DELETE_KEYWORD = "DELETE"
init(state: DamusState) {
self.state = state
let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000"
_default_zap_amount = State(initialValue: zap_amt)
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_settings = ObservedObject(initialValue: state.settings)
}
@@ -32,32 +44,200 @@ struct ConfigView: View {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
func authenticateLocally(completion: @escaping (Bool) -> Void) {
// Need to authenticate only once while ConfigView is presented
guard !has_authenticated_locally else {
completion(true)
return
}
let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
DispatchQueue.main.async {
has_authenticated_locally = success
completion(success)
}
}
} else {
// If there's no authentication set up on the device, let the user copy the key without it
has_authenticated_locally = true
completion(true)
}
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
let copyKey = {
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
self.privkey_copied = !is_pk
self.pubkey_copied = is_pk
generator.impactOccurred()
}
if is_pk {
// When trying to copy npub
copyKey()
} else {
// When trying to copy nsec
if has_authenticated_locally {
copyKey()
} else {
authenticateLocally { success in
if success {
copyKey()
}
}
}
}
}) {
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
}
}
var body: some View {
ZStack(alignment: .leading) {
Form {
Section {
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple)
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
HStack {
Text(state.keypair.pubkey_bech32)
CopyButton(is_pk: true)
}
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
if let sec = state.keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
if show_privkey == false || !has_authenticated_locally {
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
.disabled(true)
} else {
Text(sec)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
CopyButton(is_pk: false)
}
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
.onChange(of: show_privkey) { newValue in
if newValue {
authenticateLocally { success in
show_privkey = success
}
}
}
}
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
}
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.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)
}
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
selection: $settings.default_wallet) {
ForEach(Wallet.allCases, id: \.self) { wallet in
Text(wallet.model.displayName)
.tag(wallet.model.tag)
}
}
}
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
}
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
.toggleStyle(.switch)
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.translation_service == .libretranslate {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.libretranslate_server == .custom {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
}
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .libretranslate)
.autocapitalization(UITextAutocapitalizationType.none)
}
if settings.translation_service == .deepl {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .deepl)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
if settings.translation_service != .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
}
}
Section(NSLocalizedString("Miscellaneous", comment: "Section header for miscellaneous user configuration")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
.toggleStyle(.switch)
}
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
.toggleStyle(.switch)
.onChange(of: settings.disable_animation) { _ in
clear_kingfisher_cache()
}
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
.toggleStyle(.switch)
Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) {
clear_kingfisher_cache()
}
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
selection: $settings.default_media_uploader) {
ForEach(MediaUploader.allCases, id: \.self) { uploader in
Text(uploader.model.displayName)
.tag(uploader.model.tag)
}
}
}
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: {
if state.keypair.privkey == nil {
@@ -113,7 +293,7 @@ struct ConfigView: View {
}
let ev = created_deleted_account_profile(keypair: full_kp)
state.postbox.send(ev)
state.pool.send(.event(ev))
notify(.logout, ())
}
}
@@ -132,6 +312,80 @@ struct ConfigView: View {
}
}
var libretranslate_view: some View {
VStack {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
if show_api_key {
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_api_key = false
}
}
} else {
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
show_api_key = true
}
}
}
}
}
}
var deepl_view: some View {
VStack {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
HStack {
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
if show_api_key {
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
show_api_key = false
}
}
} else {
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
show_api_key = true
}
}
}
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
}
}
struct ConfigView_Previews: PreviewProvider {
+1 -10
View File
@@ -9,12 +9,9 @@ import SwiftUI
struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel()
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
@State var is_light: Bool = false
@State var is_done: Bool = false
@State var reading_eula: Bool = false
@State var profile_image: URL? = nil
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -35,7 +32,7 @@ struct CreateAccountView: View {
.font(.title.bold())
.foregroundColor(.white)
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
ProfilePictureSelector(pubkey: account.pubkey)
HStack(alignment: .top) {
VStack {
@@ -84,8 +81,6 @@ struct CreateAccountView: View {
self.is_done = true
}
.padding()
.disabled(profileUploadViewModel.isLoading)
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
}
.padding(.leading, 14.0)
.padding(.trailing, 20.0)
@@ -96,10 +91,6 @@ struct CreateAccountView: View {
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
}
func uploadedProfilePicture(image_url: URL?) {
account.profile_image = image_url?.absoluteString
}
}
struct BackNav: View {
+1 -4
View File
@@ -130,10 +130,7 @@ struct DMChatView: View {
dms.draft = ""
damus_state.postbox.send(dm)
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
damus_state.pool.send(.event(dm))
end_editing()
}
+2 -21
View File
@@ -14,18 +14,8 @@ struct DMView: View {
var is_ours: Bool {
event.pubkey == damus_state.pubkey
}
var Mention: some View {
Group {
if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) {
BuilderEventView(damus: damus_state, event_id: mention.ref.id)
} else {
EmptyView()
}
}
}
var DM: some View {
var body: some View {
HStack {
if is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
@@ -46,20 +36,11 @@ struct DMView: View {
.foregroundColor(.gray)
.opacity(0.8)
.offset(x: -10, y: -5), alignment: .bottomTrailing)
if !is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
}
}
}
var body: some View {
VStack {
Mention
DM
}
}
}
struct DMView_Previews: PreviewProvider {
+3 -11
View File
@@ -67,7 +67,6 @@ struct EditMetadataView: View {
@Environment(\.colorScheme) var colorScheme
@State var confirm_ln_address: Bool = false
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
init (damus_state: DamusState) {
self.damus_state = damus_state
@@ -103,7 +102,7 @@ struct EditMetadataView: View {
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
if let metadata_ev = m_metadata_ev {
damus_state.postbox.send(metadata_ev)
damus_state.pool.send(.event(metadata_ev))
}
}
@@ -127,7 +126,7 @@ struct EditMetadataView: View {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
@@ -202,10 +201,8 @@ struct EditMetadataView: View {
}, footer: {
if let parts = nip05_parts {
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
} else if !nip05.isEmpty {
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
} else {
Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
}
})
@@ -217,7 +214,6 @@ struct EditMetadataView: View {
dismiss()
}
}
.disabled(profileUploadViewModel.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
}
@@ -228,10 +224,6 @@ struct EditMetadataView: View {
}
.ignoresSafeArea(edges: .top)
}
func uploadedProfilePicture(image_url: URL?) {
picture = image_url?.absoluteString ?? ""
}
}
struct EditMetadataView_Previews: PreviewProvider {
@@ -1,37 +0,0 @@
//
// EmptyUserSearchView.swift
// damus
//
// Created by eric on 4/3/23.
//
//
// EmptyUserSearchView.swift
// damus
//
// Created by eric on 4/3/23.
//
import SwiftUI
struct EmptyUserSearchView: View {
var body: some View {
VStack {
Image(systemName: "person.fill.questionmark")
.font(.system(size: 35))
.padding()
Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.")
.multilineTextAlignment(.center)
.font(.callout.weight(.medium))
}
.foregroundColor(.gray)
.padding()
}
}
struct EmptyUserSearchView_Previews: PreviewProvider {
static var previews: some View {
EmptyUserSearchView()
}
}
+17 -13
View File
@@ -25,16 +25,7 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
}
}
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
switch size {
case .small:
return .preferredFont(forTextStyle: .body)
case .normal:
return .preferredFont(forTextStyle: .body)
case .selected:
return .preferredFont(forTextStyle: .title2)
}
}
struct EventView: View {
let event: NostrEvent
@@ -151,9 +142,22 @@ func format_date(_ created_at: Int64) -> String {
}
func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
let model = ActionBarModel.empty()
model.update(damus: damus, evid: ev)
return model
let likes = damus.likes.counts[ev]
let boosts = damus.boosts.counts[ev]
let zaps = damus.zaps.event_counts[ev]
let zap_total = damus.zaps.event_totals[ev]
let our_like = damus.likes.our_events[ev]
let our_boost = damus.boosts.our_events[ev]
let our_zap = damus.zaps.our_zaps[ev]
return ActionBarModel(likes: likes ?? 0,
boosts: boosts ?? 0,
zaps: zaps ?? 0,
zap_total: zap_total ?? 0,
our_like: our_like,
our_boost: our_boost,
our_zap: our_zap?.first
)
}
@@ -30,10 +30,6 @@ struct EmbeddedEventView: View {
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
}
}
+2 -2
View File
@@ -102,9 +102,9 @@ struct MenuItems: View {
}
Button(role: .destructive) {
notify(.mute, target_pubkey)
notify(.block, target_pubkey)
} label: {
Label(NSLocalizedString("Mute", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
}
}
}
+3 -11
View File
@@ -39,24 +39,17 @@ struct SelectedEventView: View {
.padding([.bottom], 4)
}
.padding(.horizontal)
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event, privkey: damus.keypair.privkey) {
ReplyDescription(event: event, profiles: damus.profiles)
.padding(.horizontal)
}
EventBody(damus_state: damus, event: event, size: size, options: [.pad_content])
EventBody(damus_state: damus, event: event, size: size, options: [])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
.padding(.horizontal)
}
Text(verbatim: "\(format_date(event.created_at))")
.padding([.top, .leading, .trailing])
.padding(.top, 10)
.font(.footnote)
.foregroundColor(.gray)
@@ -65,13 +58,11 @@ struct SelectedEventView: View {
if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
.padding(.horizontal)
Divider()
}
EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
.padding(.horizontal)
Divider()
.padding([.top], 4)
@@ -81,6 +72,7 @@ struct SelectedEventView: View {
guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target)
}
.padding([.leading], 2)
.compositingGroup()
}
}
+2 -2
View File
@@ -80,7 +80,7 @@ struct TextEvent: View {
}
.padding(.horizontal)
EvBody(options: self.options.union(.pad_content))
EvBody(options: [.truncate_content, .pad_content])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
@@ -149,7 +149,7 @@ struct TextEvent: View {
TopPart(is_anon: is_anon)
ReplyPart
EvBody(options: self.options)
EvBody(options: [])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
-120
View File
@@ -1,120 +0,0 @@
//
// ImagePicker.swift
// damus
//
// Created by Swift on 3/31/23.
//
import UIKit
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let pubkey: String
var imagesOnly: Bool = false
let onImagePicked: (URL) -> Void
let onVideoPicked: (URL) -> Void
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (URL) -> Void
private let onVideoPicked: (URL) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (URL) -> Void,
onVideoPicked: @escaping (URL) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
self.onVideoPicked = onVideoPicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
// Handle the selected video
onVideoPicked(videoURL)
} else if let imageURL = info[UIImagePickerController.InfoKey.imageURL] as? URL {
// Handle the selected image
onImagePicked(imageURL)
} else if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
if let imageURL = saveImageToTemporaryFolder(image: cameraImage, imageType: "jpeg") {
onImagePicked(imageURL)
}
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
if let editedImageURL = saveImageToTemporaryFolder(image: editedImage) {
onImagePicked(editedImageURL)
}
}
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
func saveImageToTemporaryFolder(image: UIImage, imageType: String = "png") -> URL? {
// Convert UIImage to Data
let imageData: Data?
if imageType.lowercased() == "jpeg" {
imageData = image.jpegData(compressionQuality: 1.0)
} else {
imageData = image.pngData()
}
guard let data = imageData else {
print("Failed to convert UIImage to Data.")
return nil
}
// Generate a temporary URL with a unique filename
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let uniqueImageName = "\(UUID().uuidString).\(imageType)"
let temporaryImageURL = temporaryDirectoryURL.appendingPathComponent(uniqueImageName)
// Save the image data to the temporary URL
do {
try data.write(to: temporaryImageURL)
return temporaryImageURL
} catch {
print("Error saving image data to temporary URL: \(error.localizedDescription)")
return nil
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: { url in
// Handle the selected image URL
onImagePicked(url)
},
onVideoPicked: { videoURL in
// Handle the selected video URL
onVideoPicked(videoURL)
})
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
let mediaUploader = get_media_uploader(pubkey)
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
if mediaUploader.supportsVideo && !imagesOnly {
picker.mediaTypes.append("public.movie")
}
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
+2 -6
View File
@@ -79,15 +79,11 @@ struct LoginView: View {
return
}
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
if !(BOOTSTRAP_RELAYS.contains { $0 == relay }) {
BOOTSTRAP_RELAYS.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)
notify(.login, ())
+1 -1
View File
@@ -47,7 +47,7 @@ struct TabButton: View {
.frame(width: 10, height: 10, alignment: .topTrailing)
.alignmentGuide(VerticalAlignment.center) { a in a.height + 2.0 }
.alignmentGuide(HorizontalAlignment.center) { a in a.width - 12.0 }
.foregroundColor(.accentColor)
.foregroundStyle(LINEAR_GRADIENT)
}
}
}
+2 -2
View File
@@ -26,7 +26,7 @@ struct MutelistView: View {
}
damus_state.contacts.set_mutelist(new_ev)
damus_state.postbox.send(new_ev)
damus_state.pool.send(.event(new_ev))
users = get_mutelist_users(new_ev)
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
@@ -43,7 +43,7 @@ struct MutelistView: View {
RemoveAction(pubkey: pubkey)
}
}
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
.navigationTitle(NSLocalizedString("Blocked Users", comment: "Navigation title of view to see list of blocked users."))
}
}
+6 -34
View File
@@ -29,7 +29,6 @@ struct NoteContentView: View {
let size: EventViewKind
let preview_height: CGFloat?
let options: EventViewOptions
let translatable: Bool
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable?
@@ -40,7 +39,6 @@ struct NoteContentView: View {
self.show_images = show_images
self.size = size
self.options = options
self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
@@ -65,7 +63,7 @@ struct NoteContentView: View {
}
var translateView: some View {
TranslateView(damus_state: damus_state, event: event, size: self.size)
TranslateView(damus_state: damus_state, event: event)
}
var previewView: some View {
@@ -87,12 +85,7 @@ struct NoteContentView: View {
var MainContent: some View {
VStack(alignment: .leading) {
if size == .selected {
if with_padding {
SelectableText(attributedString: artifacts.content, size: self.size)
.padding(.horizontal)
} else {
SelectableText(attributedString: artifacts.content, size: self.size)
}
SelectableText(attributedString: artifacts.content)
} else {
if with_padding {
truncatedText
@@ -102,7 +95,7 @@ struct NoteContentView: View {
}
}
if translatable {
if size == .selected || damus_state.settings.auto_translate {
if with_padding {
translateView
.padding(.horizontal)
@@ -112,10 +105,10 @@ struct NoteContentView: View {
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
ImageCarousel(urls: artifacts.images)
Blur()
.disabled(true)
}
@@ -262,10 +255,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
.filter({ $0.is_note_mention })
.count == 1
var ind: Int = -1
let txt: AttributedString = blocks.reduce("") { str, block in
ind = ind + 1
switch block {
case .mention(let m):
if m.type == .event && one_note_ref {
@@ -273,14 +263,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
}
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) {
trimmed = " " + trim_prefix(trimmed)
}
if let next = blocks[safe: ind+1], case .url(let u) = next, is_image_url(u) {
trimmed = trim_suffix(trimmed)
}
return str + AttributedString(stringLiteral: trimmed)
return str + AttributedString(stringLiteral: txt)
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
@@ -358,14 +341,3 @@ struct TruncatedText: View {
return AttributedString(truncatedAttributedString) + "..."
}
}
// trim suffix whitespace and newlines
func trim_suffix(_ str: String) -> String {
return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
}
// trim prefix whitespace and newlines
func trim_prefix(_ str: String) -> String {
return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
}
@@ -73,7 +73,7 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
}
if zap.is_anon {
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
return "Anonymous"
}
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
@@ -212,9 +212,8 @@ struct EventGroupView: View {
NavigationLink(destination: dest) {
VStack(alignment: .leading) {
GroupDescription
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])
EventBody(damus_state: state, event: event, size: .normal, options: [])
.padding([.top], 1)
.padding([.trailing])
.foregroundColor(.gray)
}
}
@@ -35,14 +35,6 @@ struct NotificationItemView: View {
notification_item_event(events: state.events, notif: item)
}
var options: EventViewOptions {
if state.settings.truncate_mention_text {
return [.wide, .truncate_content]
}
return [.wide]
}
func Item(_ ev: NostrEvent?) -> some View {
Group {
switch item {
@@ -60,12 +52,13 @@ struct NotificationItemView: View {
case .reply(let ev):
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev, options: options)
EventView(damus: state, event: ev)
}
.buttonStyle(.plain)
}
ThiccDivider()
Divider()
.padding([.top,.bottom], 5)
}
}
@@ -91,6 +91,7 @@ struct NotificationsView: View {
}
return Color.clear
})
.padding(.horizontal)
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in
+5 -16
View File
@@ -16,34 +16,23 @@ struct ParticipantsView: View {
var body: some View {
VStack {
Text("Replying to", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
.font(.headline)
Text("Edit participants", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
HStack {
Spacer()
Button {
// Remove all "p" refs, keep "e" refs
references = originalReferences.eRefs
} label: {
Text("Remove all", comment: "Button label to remove all participants from a note reply.")
}
.font(.system(size: 14, weight: .bold))
.frame(width: 100, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.buttonStyle(.borderedProminent)
Spacer()
Button {
references = originalReferences
} label: {
Text("Add all", comment: "Button label to re-add all original participants as profiles to reply to in a note")
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.buttonStyle(.borderedProminent)
Spacer()
}
VStack {
@@ -67,7 +56,7 @@ struct ParticipantsView: View {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray)
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
+71 -97
View File
@@ -22,12 +22,10 @@ struct PostView: View {
@State var attach_camera: Bool = false
@State var error: String? = nil
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -80,7 +78,6 @@ struct PostView: View {
attach_media = true
}, label: {
Image(systemName: "photo")
.padding(6)
})
}
@@ -89,7 +86,6 @@ struct PostView: View {
attach_camera = true
}, label: {
Image(systemName: "camera")
.padding(6)
})
}
@@ -109,12 +105,10 @@ struct PostView: View {
self.send_post()
}
}
.disabled(is_post_empty)
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.opacity(is_post_empty ? 0.5 : 1.0)
.clipShape(Capsule())
}
@@ -156,7 +150,9 @@ struct PostView: View {
Spacer()
PostButton
if !is_post_empty {
PostButton
}
}
if let progress = image_upload.progress {
@@ -165,7 +161,7 @@ struct PostView: View {
}
}
.frame(height: 30)
.padding([.bottom], 10)
.padding([.top, .bottom], 4)
}
func append_url(_ url: String) {
@@ -204,97 +200,75 @@ struct PostView: View {
}
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(post.string)
TopBar
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: 45.0, highlight: .none, profiles: damus_state.profiles)
let searching = get_searching_string(post.string)
TextEntry
}
.frame(maxHeight: searching == nil ? .infinity : 50)
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
TopBar
ScrollViewReader { scroller in
ScrollView {
if let replying_to = replying_to {
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
}
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
.padding(.leading, replying_to != nil ? 15 : 0)
TextEntry
}
.frame(height: deviceSize.size.height*0.78)
.id("post")
}
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
}
}
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post)
.padding(.leading, replying_to != nil ? 15 : 0)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
VStack(alignment: .leading) {
AttachmentBar
}
}
AttachmentBar
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.sheet(isPresented: $attach_camera) {
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.onAppear() {
if let replying_to {
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
}
} else {
post = damus_state.drafts.post
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focus = true
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, damusState: damus_state) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.sheet(isPresented: $attach_camera) {
ImagePicker(sourceType: .camera, damusState: damus_state) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.onAppear() {
if let replying_to {
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
}
} else {
post = damus_state.drafts.post
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focus = true
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
}
@@ -321,6 +295,6 @@ func get_searching_string(_ post: String) -> String? {
struct PostView_Previews: PreviewProvider {
static var previews: some View {
PostView(replying_to: nil, damus_state: test_damus_state())
PostView(replying_to: nil, references: [], damus_state: test_damus_state())
}
}
+5 -10
View File
@@ -74,16 +74,11 @@ struct UserSearch: View {
var body: some View {
ScrollView {
LazyVStack {
Divider()
if users.count == 0 {
EmptyUserSearchView()
} else {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
on_user_tapped(user: user)
}
}
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
on_user_tapped(user: user)
}
}
}
}
@@ -1,84 +0,0 @@
//
// ProfilePictureEditView.swift
// damus
//
// Created by Joel Klabo on 3/30/23.
//
import SwiftUI
struct EditProfilePictureControl: View {
let pubkey: String
@Binding var profile_image: URL?
@ObservedObject var viewModel: ProfileUploadingViewModel
let callback: (URL?) -> Void
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
@State private var show_camera = false
@State private var show_library = false
var body: some View {
Menu {
Button(action: {
self.show_library = true
}) {
Text("Choose from Library", comment: "Option to select photo from library")
}
Button(action: {
self.show_camera = true
}) {
Text("Take Photo", comment: "Option to take a photo with the camera")
}
} label: {
if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "camera")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(DamusColors.white)
}
}
.sheet(isPresented: $show_camera) {
ImagePicker(sourceType: .camera, pubkey: pubkey, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
}
.sheet(isPresented: $show_library) {
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
}
}
private func handle_upload(media: MediaUpload) {
viewModel.isLoading = true
let uploader = get_media_uploader(pubkey)
Task {
let res = await image_upload.start(media: media, uploader: uploader)
switch res {
case .success(let urlString):
let url = URL(string: urlString)
profile_image = url
callback(url)
case .failed(let error):
if let error {
print("Error uploading profile image \(error.localizedDescription)")
} else {
print("Error uploading image :(")
}
callback(nil)
}
viewModel.isLoading = false
}
}
}
+1 -55
View File
@@ -32,60 +32,6 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
}
}
struct EditProfilePictureView: View {
@Binding var url: URL?
let pubkey: String
let size: CGFloat
let highlight: Highlight
var damus_state: DamusState?
var PlaceholderColor: Color {
return id_to_color(pubkey)
}
var Placeholder: some View {
PlaceholderColor
.frame(width: size, height: size)
.clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.padding(2)
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
KFAnimatedImage(get_profile_url())
.imageContext(.pfp)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Placeholder
}
.scaledToFill()
.opacity(0.5)
}
.frame(width: size, height: size)
.clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
}
private func get_profile_url() -> URL? {
if let url {
return url
} else if let state = damus_state, let picture = state.profiles.lookup(id: pubkey)?.picture {
return URL(string: picture)
} else {
return url ?? URL(string: robohash(pubkey))
}
}
}
struct InnerProfilePicView: View {
let url: URL?
@@ -172,7 +118,7 @@ func make_preview_profiles(_ pubkey: String) -> Profiles {
let profiles = Profiles()
let picture = "http://cdn.jb55.com/img/red-me.jpg"
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com")
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0)
profiles.add(id: pubkey, profile: ts_profile)
return profiles
}
@@ -7,27 +7,13 @@
import SwiftUI
import Combine
class ProfileUploadingViewModel: ObservableObject {
@Published var isLoading: Bool = false
}
struct ProfilePictureSelector: View {
let pubkey: String
var size: CGFloat = 80.0
var damus_state: DamusState?
@ObservedObject var viewModel: ProfileUploadingViewModel
let callback: (URL?) -> Void
@State var profile_image: URL? = nil
var body: some View {
let highlight: Highlight = .custom(Color.white, 2.0)
ZStack {
EditProfilePictureView(url: $profile_image, pubkey: pubkey, size: size, highlight: highlight, damus_state: damus_state)
EditProfilePictureControl(pubkey: pubkey, profile_image: $profile_image, viewModel: viewModel, callback: callback)
ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles())
}
}
}
@@ -35,8 +21,6 @@ struct ProfilePictureSelector: View {
struct ProfilePictureSelector_Previews: PreviewProvider {
static var previews: some View {
let test_pubkey = "ff48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
ProfilePictureSelector(pubkey: test_pubkey, viewModel: ProfileUploadingViewModel()) { _ in
//
}
ProfilePictureSelector(pubkey: test_pubkey)
}
}
+4 -4
View File
@@ -221,8 +221,8 @@ struct ProfileView: View {
notify(.report, target)
}
Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) {
notify(.mute, profile.pubkey)
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
@@ -480,7 +480,7 @@ func test_damus_state() -> DamusState {
let damus = DamusState.empty
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)
damus.profiles.add(id: pubkey, profile: tsprof)
return damus
}
@@ -541,7 +541,7 @@ struct KeyView: View {
} icon: {
Image(systemName: "square.on.square.dashed")
.contentShape(Rectangle())
.foregroundColor(.accentColor)
.foregroundColor(.gray)
.frame(width: 20, height: 20)
}
.labelStyle(IconOnlyLabelStyle())
@@ -111,7 +111,7 @@ struct RecommendedRelayView: View {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.postbox.send(ev_after_add)
damus.pool.send(.event(ev_after_add))
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ struct RelayConfigView: View {
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = URL(string: x) {
xs.append(RelayDescriptor(url: url, info: .rw))
if state.pool.get_relay(x) == nil {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ struct RelayDetailView: View {
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
state.pool.send(.event(new_ev))
dismiss()
}) {
Text("Disconnect From Relay", comment: "Button to disconnect from the relay.")
@@ -60,7 +60,7 @@ struct RelayDetailView: View {
return
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
state.pool.send(.event(ev_after_add))
dismiss()
}) {
Text("Connect To Relay", comment: "Button to connect to the relay.")
+1 -1
View File
@@ -82,7 +82,7 @@ struct RelayView: View {
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
state.pool.send(.event(new_ev))
}) {
if showText {
Text(NSLocalizedString("Disconnect", comment: "Button to disconnect from a relay server."))
+46 -49
View File
@@ -7,74 +7,71 @@
import SwiftUI
func all_referenced_pubkeys(_ ev: NostrEvent) -> [ReferencedId] {
var keys = ev.referenced_pubkeys
let ref = ReferencedId(ref_id: ev.pubkey, relay_id: nil, key: "p")
keys.insert(ref, at: 0)
return keys
}
struct ReplyView: View {
let replying_to: NostrEvent
let damus: DamusState
@Binding var originalReferences: [ReferencedId]
@Binding var references: [ReferencedId]
@State var participantsShown: Bool = false
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
var ReplyingToSection: some View {
HStack {
Group {
@State var participantsShown: Bool = false
var body: some View {
VStack {
Text("Replying to:", comment: "Indicating that the user is replying to the following listed people.")
HStack(alignment: .top) {
let names = references.pRefs
.map { pubkey in
let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk)
return "@" + Profile.displayName(profile: prof, pubkey: pk).username
return Profile.displayName(profile: prof, pubkey: pk).username
}
.joined(separator: " ")
if names.isEmpty {
Text("Replying to \(Text("self", comment: "Part of a larger sentence 'Replying to self' in US English. 'self' indicates that the user is replying to themself and no one else.").foregroundColor(.accentColor).font(.footnote))", comment: "Indicating that the user is replying to the themself and no one else, where the parameter is 'self' in US English.")
.foregroundColor(.gray)
.font(.footnote)
} else {
Text("Replying to \(Text(verbatim: names).foregroundColor(.accentColor).font(.footnote))", comment: "Indicating that the user is replying to the following listed people.")
.foregroundColor(.gray)
.font(.footnote)
}
.joined(separator: ", ")
Text(names)
.foregroundColor(.gray)
.font(.footnote)
}
.onTapGesture {
participantsShown.toggle()
}
.sheet(isPresented: $participantsShown) {
if #available(iOS 16.0, *) {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
}
ScrollViewReader { scroller in
ScrollView {
EventView(damus: damus, event: replying_to, options: [.no_action_bar])
PostView(replying_to: replying_to, references: references, damus_state: damus)
.frame(minHeight: 500, maxHeight: .infinity)
.id("post")
}
.frame(maxHeight: .infinity)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
}
}
.padding(.leading, 75)
Spacer()
}
.padding()
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
originalReferences = references
}
}
var body: some View {
VStack(alignment: .leading) {
}
EventView(damus: damus, event: replying_to, options: [.no_action_bar])
.padding()
.background(GeometryReader { geometry in
let eventHeight = geometry.frame(in: .global).height
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: eventHeight + 7)
.offset(x: 25, y: 40)
.padding(.leading)
})
ReplyingToSection
.background(GeometryReader { geometry in
let replyingToHeight = geometry.frame(in: .global).height
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: replyingToHeight)
.offset(x: 25, y: 40)
.padding(.leading)
})
}
struct ReplyView_Previews: PreviewProvider {
static var previews: some View {
ReplyView(replying_to: NostrEvent(content: "hi", pubkey: "pubkey"), damus: test_damus_state(), references: [])
}
}
+6 -6
View File
@@ -8,7 +8,7 @@
import SwiftUI
struct ReportView: View {
let postbox: PostBox
let pool: RelayPool
let target: ReportTarget
let privkey: String
@@ -44,7 +44,7 @@ struct ReportView: View {
}
func do_send_report(type: ReportType) {
guard let ev = send_report(privkey: privkey, postbox: postbox, target: target, type: type) else {
guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: type) else {
return
}
@@ -92,12 +92,12 @@ struct ReportView: View {
}
}
func send_report(privkey: String, postbox: PostBox, target: ReportTarget, type: ReportType) -> NostrEvent? {
func send_report(privkey: String, pool: RelayPool, target: ReportTarget, type: ReportType) -> NostrEvent? {
let report = Report(type: type, target: target, message: "")
guard let ev = create_report_event(privkey: privkey, report: report) else {
return nil
}
postbox.send(ev)
pool.send(.event(ev))
return ev
}
@@ -106,9 +106,9 @@ struct ReportView_Previews: PreviewProvider {
let ds = test_damus_state()
VStack {
ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "")
ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "")
ReportView(postbox: ds.postbox, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id")
ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id")
}
}
+5 -8
View File
@@ -90,8 +90,7 @@ struct SaveKeysView: View {
}
func complete_account_creation(_ account: CreateAccountModel) {
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
for relay in bootstrap_relays {
for relay in BOOTSTRAP_RELAYS {
add_rw_relay(self.pool, relay)
}
@@ -108,13 +107,13 @@ struct SaveKeysView: View {
switch wsev {
case .connected:
let metadata = create_account_to_metadata(account)
let metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata)
let contacts_ev = make_first_contact_event(keypair: account.keypair)
let m_metadata_ev = make_metadata_event(keypair: account.keypair, metadata: metadata)
let m_contacts_ev = make_first_contact_event(keypair: account.keypair)
if let metadata_ev {
if let metadata_ev = m_metadata_ev {
self.pool.send(.event(metadata_ev))
}
if let contacts_ev {
if let contacts_ev = m_contacts_ev {
self.pool.send(.event(contacts_ev))
}
@@ -142,8 +141,6 @@ struct SaveKeysView: View {
print("event in signup?")
case .eose:
break
case .ok:
break
}
}
}
+5 -8
View File
@@ -15,6 +15,8 @@ struct SearchHomeView: View {
@State var search: String = ""
@FocusState private var isFocused: Bool
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var SearchInput: some View {
HStack {
HStack{
@@ -31,7 +33,7 @@ struct SearchHomeView: View {
if(!search.isEmpty) {
Text("Cancel", comment: "Cancel out of search view.")
.foregroundColor(.accentColor)
.foregroundStyle(LINEAR_GRADIENT)
.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0))
.onTapGesture {
self.search = ""
@@ -52,17 +54,12 @@ struct SearchHomeView: View {
return true
}
// Always show your own posts.
if $0.pubkey == damus_state.pubkey {
return true
}
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
guard let noteLanguage = damus_state.translations.detectLanguage($0, state: damus_state) else {
guard let noteLanguage = $0.note_language(damus_state.keypair.privkey) else {
return true
}
return damus_state.translations.preferredLanguages.contains(noteLanguage)
return preferredLanguages.contains(noteLanguage)
}
)
.refreshable {
@@ -1,65 +0,0 @@
//
// TextFormattingSettings.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
struct AppearanceSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
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)
Toggle(NSLocalizedString("Truncate notification mention text", comment: "Setting to truncate text in mention notifications"), isOn: $settings.truncate_mention_text)
.toggleStyle(.switch)
}
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)
}
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
.toggleStyle(.switch)
.onChange(of: settings.disable_animation) { _ in
clear_kingfisher_cache()
}
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
.toggleStyle(.switch)
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
selection: $settings.default_media_uploader) {
ForEach(MediaUploader.allCases, id: \.self) { uploader in
Text(uploader.model.displayName)
.tag(uploader.model.tag)
}
}
Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) {
clear_kingfisher_cache()
}
}
}
.navigationTitle("Appearance")
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
}
struct TextFormattingSettings_Previews: PreviewProvider {
static var previews: some View {
AppearanceSettingsView(settings: UserSettingsStore())
}
}
-135
View File
@@ -1,135 +0,0 @@
//
// KeySettingsView.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
import LocalAuthentication
struct KeySettingsView: View {
let keypair: Keypair
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var show_privkey: Bool = false
@State var has_authenticated_locally: Bool = false
@Environment(\.dismiss) var dismiss
init(keypair: Keypair) {
_privkey = State(initialValue: keypair.privkey_bech32 ?? "")
self.keypair = keypair
}
var ShowSecToggle: some View {
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
.onChange(of: show_privkey) { newValue in
if newValue {
authenticate_locally(has_authenticated_locally) { success in
self.has_authenticated_locally = success
self.show_privkey = success
}
}
}
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
let copyKey = {
UIPasteboard.general.string = is_pk ? self.keypair.pubkey_bech32 : self.privkey
self.privkey_copied = !is_pk
self.pubkey_copied = is_pk
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
}
if is_pk {
copyKey()
return
}
if has_authenticated_locally {
copyKey()
return
}
authenticate_locally(has_authenticated_locally) { success in
self.has_authenticated_locally = success
if success {
copyKey()
}
}
}) {
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
}
}
var body: some View {
Form {
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
HStack {
Text(keypair.pubkey_bech32)
CopyButton(is_pk: true)
}
.clipShape(RoundedRectangle(cornerRadius: 5))
}
if let sec = keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
if show_privkey == false || !has_authenticated_locally {
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
.disabled(true)
} else {
Text(sec)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
CopyButton(is_pk: false)
}
ShowSecToggle
}
}
}
.navigationTitle("Keys")
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
}
struct KeySettingsView_Previews: PreviewProvider {
static var previews: some View {
let kp = generate_new_keypair()
KeySettingsView(keypair: kp)
}
}
func authenticate_locally(_ has_authenticated_locally: Bool, completion: @escaping (Bool) -> Void) {
// Need to authenticate only once while ConfigView is presented
guard !has_authenticated_locally else {
completion(true)
return
}
let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
DispatchQueue.main.async {
completion(success)
}
}
} else {
// If there's no authentication set up on the device, let the user copy the key without it
completion(true)
}
}
@@ -1,47 +0,0 @@
//
// NotificationSettings.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
struct NotificationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: $settings.mention_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: $settings.repost_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: $settings.like_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: $settings.dm_notification)
.toggleStyle(.switch)
}
Section(header: Text(NSLocalizedString("Notification Preference", comment: "Section header for Notification Preferences"))) {
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
.toggleStyle(.switch)
}
}
.navigationTitle("Notifications")
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
}
struct NotificationSettings_Previews: PreviewProvider {
static var previews: some View {
NotificationSettingsView(settings: UserSettingsStore())
}
}
@@ -1,160 +0,0 @@
//
// TranslationSettingsView.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
struct TranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@State var show_api_key: Bool = false
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
.toggleStyle(.switch)
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.translation_service == .libretranslate {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.libretranslate_server == .custom {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
}
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .libretranslate)
.autocapitalization(UITextAutocapitalizationType.none)
}
if settings.translation_service == .deepl {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .deepl)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
if settings.translation_service != .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
}
}
}
.navigationTitle("Translation")
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
var libretranslate_view: some View {
VStack {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
if show_api_key {
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_api_key = false
}
}
} else {
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
show_api_key = true
}
}
}
}
}
}
var deepl_view: some View {
VStack {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
HStack {
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
if show_api_key {
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
show_api_key = false
}
}
} else {
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
show_api_key = true
}
}
}
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
}
}
struct TranslationSettingsView_Previews: PreviewProvider {
static var previews: some View {
TranslationSettingsView(settings: UserSettingsStore())
}
}
@@ -1,66 +0,0 @@
//
// WalletSettingsView.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
import Combine
struct ZapSettingsView: View {
let pubkey: String
@ObservedObject var settings: UserSettingsStore
@State var default_zap_amount: String
@Environment(\.dismiss) var dismiss
init(pubkey: String, settings: UserSettingsStore) {
self.pubkey = pubkey
let zap_amt = get_default_zap_amount(pubkey: pubkey).map({ "\($0)" }) ?? "1000"
_default_zap_amount = State(initialValue: zap_amt)
self._settings = ObservedObject(initialValue: settings)
}
var body: some View {
Form {
Section("Wallet") {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
selection: $settings.default_wallet) {
ForEach(Wallet.allCases, id: \.self) { wallet in
Text(wallet.model.displayName)
.tag(wallet.model.tag)
}
}
}
Section("Zaps") {
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
.toggleStyle(.switch)
}
Section("Default Zap Amount in sats") {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
set_default_zap_amount(pubkey: self.pubkey, amount: parsed)
}
}
}
}
.navigationTitle("Zaps")
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
}
}
struct WalletSettingsView_Previews: PreviewProvider {
static var previews: some View {
ZapSettingsView(pubkey: "pubkey", settings: UserSettingsStore())
}
}
+1 -1
View File
@@ -93,7 +93,7 @@ struct SideMenuView: View {
*/
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), systemImage: "exclamationmark.octagon")
navLabel(title: NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon")
}
NavigationLink(destination: RelayConfigView(state: damus_state)) {
-1
View File
@@ -13,7 +13,6 @@ struct TextViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.showsVerticalScrollIndicator = false
TextViewWrapper.setTextProperties(textView)
return textView
}
+4 -5
View File
@@ -30,8 +30,8 @@ struct ThreadView: View {
MutedEventView(damus_state: state,
event: parent_event,
scroller: reader,
selected: false)
.padding(.horizontal)
selected: false
)
.onTapGesture {
thread.set_active_event(parent_event, privkey: state.keypair.privkey)
scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false)
@@ -49,7 +49,7 @@ struct ThreadView: View {
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: eventHeight)
.offset(x: 40, y: 40)
.offset(x: 25, y: 40)
})
// MARK: - Actual event view
@@ -68,7 +68,6 @@ struct ThreadView: View {
scroller: nil,
selected: false
)
.padding(.horizontal)
.onTapGesture {
thread.set_active_event(child_event, privkey: state.keypair.privkey)
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
@@ -77,7 +76,7 @@ struct ThreadView: View {
Divider()
.padding([.top], 4)
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
.onAppear {
thread.subscribe()
+2 -10
View File
@@ -25,14 +25,6 @@ struct InnerTimelineView: View {
self._nav_target = State(initialValue: test_event)
}
var event_options: EventViewOptions {
if self.damus.settings.truncate_timeline_text {
return [.wide, .truncate_content]
}
return [.wide]
}
var body: some View {
let thread = ThreadModel(event: nav_target, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
@@ -45,14 +37,14 @@ struct InnerTimelineView: View {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
EventView(damus: damus, event: ev, options: event_options)
EventView(damus: damus, event: ev, options: [.wide])
.onTapGesture {
nav_target = ev.inner_event ?? ev
navigating = true
}
.padding(.top, 7)
ThiccDivider()
Divider()
.padding([.top], 7)
}
}
+3 -3
View File
@@ -36,15 +36,15 @@ struct UserRelaysView: View {
.onReceive(handle_notify(.relays_changed)) { _ in
self.relay_state = UserRelaysView.make_relay_state(pool: state.pool, relays: self.relays)
}
.navigationBarTitle(NSLocalizedString("Relays", comment: "Navigation bar title that shows the list of relays for a user."))
.navigationBarTitle("Relays")
.toolbar{
if state.keypair.privkey != nil {
if showAddButton {
Button(NSLocalizedString("Done", comment: "Button that, when tapped, will finish adding a different user's relays to your relay by hiding the + buttons next to the relays.")) {
Button("Done") {
showAddButton.toggle()
}
} else {
Button(NSLocalizedString("Show +", comment: "Button that, when tapped, will show + buttons next to a user's relays.")) {
Button("Show Add") {
showAddButton.toggle()
}
}
Binary file not shown.
Binary file not shown.
-48
View File
@@ -338,54 +338,6 @@
<string>%2$@ ساتوشي</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>zero</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
<key>one</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
<key>two</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
<key>few</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
<key>many</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
<key>other</key>
<string>تم استلام %2$@ ساتوشي من %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>zero</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
<key>one</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
<key>two</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
<key>few</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
<key>many</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
<key>other</key>
<string>تم استلام %2$@ من %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-40
View File
@@ -282,46 +282,6 @@
<string>%2$@ satů</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Dostal jsi %2$@ sat od %3$@</string>
<key>few</key>
<string>Dostal jsi %2$@ satů od %3$@</string>
<key>many</key>
<string>Dostal jsi %2$@ satů od %3$@</string>
<key>other</key>
<string>Dostal jsi %2$@ satů od %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Dostal jsi %2$@ sat od %3$@: "%4$@"</string>
<key>few</key>
<string>Dostal jsi %2$@ satů od %3$@: "%4$@"</string>
<key>many</key>
<string>Dostal jsi %2$@ satů od %3$@: "%4$@"</string>
<key>other</key>
<string>Dostal jsi %2$@ satů od %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-32
View File
@@ -226,38 +226,6 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Du hast %2$@ Sat von %3$@ erhalten</string>
<key>other</key>
<string>Du hast %2$@ Sats von %3$@ erhalten</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Du hast %2$@ Sat von %3$@ erhalten: "%4$@"</string>
<key>other</key>
<string>Du hast %2$@ Sats von %3$@ erhalten: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-32
View File
@@ -226,38 +226,6 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Λάβατε %2$@ sat από %3$@</string>
<key>other</key>
<string>Λάβατε %2$@ sats από %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Λάβατε %2$@ sat από %3$@: "%4$@"</string>
<key>other</key>
<string>Λάβατε %2$@ sats από %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
-32
View File
@@ -226,38 +226,6 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>You received %2$@ sat from %3$@</string>
<key>other</key>
<string>You received %2$@ sats from %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>You received %2$@ sat from %3$@: "%4$@"</string>
<key>other</key>
<string>You received %2$@ sats from %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

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