Compare commits

...

62 Commits

Author SHA1 Message Date
tyiu 5ddb684fae Add ScrollView to ParticipantsView and fix filename typo 2023-02-20 22:11:51 -05:00
William Casarin af6f88ab17 Fix moving post button to quell jack's OCD 2023-02-20 15:04:06 -08:00
William Casarin 647495dbc0 Fix bug where feed sometimes gets reset to realtime when scrolling 2023-02-20 14:58:10 -08:00
William Casarin 826fd1ef33 More consistent scrolling to top behavior 2023-02-20 14:26:45 -08:00
William Casarin 54dd2035a1 Always flush events when switching timelines 2023-02-20 14:21:21 -08:00
William Casarin 587819c8eb Always switch to realtime mode on scroll-to-top, remove realtime indicator 2023-02-20 13:51:54 -08:00
William Casarin 8954c1c245 Remove load more popup 2023-02-20 13:48:36 -08:00
William Casarin 19a421604c Remove all localization from formatting strings
until we have test converage
2023-02-20 12:44:43 -08:00
William Casarin 68b57d8b99 Fix localization crash 2023-02-20 12:40:34 -08:00
William Casarin f3056653db v1.1.0-3 changelog 2023-02-20 11:36:59 -08:00
William Casarin 6196279d2b v1.1.0-3 2023-02-20 11:35:44 -08:00
William Casarin f213420b41 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-20 11:34:28 -08:00
William Casarin b4140dc5f2 Add a "load more" button instead of always inserting events in timelines
Changelog-Added: Add a "load more" button instead of always inserting events in timelines
2023-02-20 11:12:31 -08:00
tyiu 1b27e9041f Fix localization issues 2023-02-19 13:17:53 -05:00
William Casarin 795577a0a1 Rename Global feed to Universe
Changelog-Changed: Rename global feed to universe
2023-02-19 09:53:24 -08:00
William Casarin d5c45dc8ba Fix rare markdown crash 2023-02-19 09:20:09 -08:00
Bryan Montz 603a5a1814 Refinements to RelayConnection plus tests for creating requests
Closes: #644
2023-02-19 08:36:03 -08:00
tyiu 06a1a9aba6 Fix Zaps string pluralization bug
Closes: #646
2023-02-19 08:31:43 -08:00
Bryan Montz ff1815cce0 refactor similar RepostsModel and ReactionsModel into one parent class
Closes: #650
2023-02-19 08:26:30 -08:00
OlegAba 0bdec912f8 Restrict dynamic font type - max size 2023-02-18 09:34:50 -08:00
William Casarin 6d8312fa57 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-18 09:33:54 -08:00
Bryan Montz f6d56179eb Image and color asset clean-up
Closes: #643
2023-02-18 09:29:20 -08:00
Bryan Montz 193e922c9c code clean-up: @discardableResult, unused params, simplify getting specific relays from pool
Closes: #635
2023-02-18 09:22:09 -08:00
OlegAba a1a89dc98e Add selectable text feature
Changelog-Added: Added the ability to select text on posts
Closes: #639
2023-02-18 08:59:47 -08:00
ericholguin 3e764e75e4 Post view improvements
Changelog-Changed: Improve look of post view
Closes: #561
2023-02-17 10:30:46 -08:00
William Casarin 7c563cb0ae Revert "Add menu ellipsis button to notes"
This reverts commit 390c9162ae.
2023-02-17 10:24:47 -08:00
tyiu a328b0d1a8 Import translations 2023-02-17 09:29:56 -05:00
OlegAba 5018b9aa1e Added a 20MB content length limit for all image files
Changelog-Changed: Added a 20MB content length limit for all image files
Closes: #335
2023-02-16 12:19:18 -08:00
middlingphys 1f6657e471 Remove trailing slash when adding a relay
Changelog-Fixed: Remove trailing slash when adding a relay
Closes: #562
2023-02-16 12:17:04 -08:00
ericholguin 062b5dc040 Added Posts or Post & Replies selector to Profile
Changelog-Added: Added Posts or Post & Replies selector to Profile
Closes: #496
2023-02-16 08:48:23 -08:00
ericholguin 390c9162ae Add menu ellipsis button to notes
Changelog-Changed: Switch from long-press to ... on events for context menu
Closes: #568
2023-02-16 08:37:54 -08:00
Bryan Montz 94f66adf8d Improve EventActionBar button spacing
Changelog-Changed: Improved EventActionBar button spacing
Closes: #576
2023-02-16 08:34:22 -08:00
OlegAba d547dade04 Use top anchor for scroll to top event
Changelog-Fixed: Scroll to top of events instead of the bottom
Closes: #570
2023-02-16 08:29:25 -08:00
OlegAba 94a67adff9 Fix padding 2023-02-16 08:29:21 -08:00
William Casarin 29f192c377 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-16 07:29:37 -08:00
tyiu 4e67c88607 Export and import translations 2023-02-16 10:27:00 -05:00
William Casarin 42200c347b Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-16 07:22:01 -08:00
tyiu 36f05ccaed Export and import translations 2023-02-16 10:17:09 -05:00
tyiu 98a1b95d12 Use Text(verbatim:) to indicate non-translatable strings 2023-02-16 10:15:38 -05:00
Bryan Montz 4cdef502e9 profile: copy button polish
- Updated checkmark icon to SF Symbols
- Updated copy icon to one from SF Symbols

Changelog-Changed: Polished profile key copy buttons, added animation
Closes: #619
2023-02-16 06:13:59 -08:00
Joel Klabo ae2e70ba7d Format Large Numbers of Action Bar Actions
Changelog-Changed: Format large numbers of action bar actions
Closes: #626
2023-02-16 06:09:09 -08:00
Bryan Montz 1b4e54582f fixed tests 2023-02-16 06:07:04 -08:00
William Casarin 909148f0be add missing file 2023-02-15 19:15:19 -08:00
OlegAba c100c6db47 Merge remote-tracking branch 'oleg/custom-profile-navbar'
Changelog-Added: Improved profile navbar
2023-02-15 12:32:25 -08:00
Bryan Montz 8d3fb397f7 Improved blur on images, especially in dark mode
Changelog-Changed: Improved blur on images, especially in dark mode
Closes: #583
2023-02-15 11:19:09 -08:00
William Casarin f8742a609c Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 11:17:42 -08:00
William Casarin d55d0d61ed perf: debounce incoming dms
This fixes perf issues on startup if you have lots of dms

Changelog-Fixed: Fix lag on startup when you have lots of DMs
Changelog-Fixed: Fix an issues where dm notifications appear without any new events
2023-02-15 11:14:13 -08:00
William Casarin cf90480501 perf: decode large events in background threads
should help with hitches a bit
2023-02-15 11:14:13 -08:00
OlegAba f0075904c2 Fix frequent KFImage hang
Changelog-Fixed: Fix some hangs when scrolling by images
Closes: #614
2023-02-15 11:13:04 -08:00
tyiu a41acc12e7 Import translations 2023-02-15 12:50:57 -05:00
William Casarin 1e22984d52 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 08:49:45 -08:00
tyiu 9080e4efae Force default zap amount text field to accept only numbers
Changelog-Fixed: Force default zap amount text field to accept only numbers
Closes: #612
2023-02-15 08:47:22 -08:00
tyiu 6488634eda Import translations 2023-02-15 10:54:45 -05:00
tyiu 355cd1283c Wrap non-translatable strings so that they do not get exported 2023-02-15 10:44:44 -05:00
William Casarin 6ed9c408f9 v1.1.0-2 changelog 2023-02-14 10:17:08 -08:00
William Casarin 5f52e6f62f v1.1.0-2 2023-02-14 10:15:40 -08:00
William Casarin 59211bb4fd Ensure stats get updated in realtime on action bars
Changelog-Fixed: Ensure stats get updated in realtime on action bars
2023-02-14 10:05:59 -08:00
William Casarin 6d634763c5 Fix repost counters
Changelog-Fixed: Fix reposts not getting counted properly
2023-02-14 10:05:19 -08:00
OlegAba 2366089896 Fix vertical spacing bug 2023-02-10 16:52:34 -05:00
OlegAba 9a95967a81 Refactor pfp image view to use zoomable scroll view 2023-02-10 01:03:55 -05:00
OlegAba 504108da75 Add custom profile navbar 2023-02-09 18:24:16 -05:00
OlegAba d43a2ff92d Move safeAreaInset ref to Theme 2023-02-09 18:22:48 -05:00
162 changed files with 4154 additions and 24400 deletions
+52
View File
@@ -1,3 +1,54 @@
## [1.1.0-3] - 2023-02-20
### Added
- Add a "load more" button instead of always inserting events in timelines (William Casarin)
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
### Changed
- Rename global feed to universe (William Casarin)
- Improve look of post view (ericholguin)
- Added a 20MB content length limit for all image files (OlegAba)
- Improved EventActionBar button spacing (Bryan Montz)
- Polished profile key copy buttons, added animation (Bryan Montz)
- Format large numbers of action bar actions (Joel Klabo)
- Improved blur on images, especially in dark mode (Bryan Montz)
### Fixed
- Remove trailing slash when adding a relay (middlingphys)
- Scroll to top of events instead of the bottom (OlegAba)
- Fix lag on startup when you have lots of DMs (William Casarin)
- Fix an issues where dm notifications appear without any new events (William Casarin)
- Fix some hangs when scrolling by images (OlegAba)
- Force default zap amount text field to accept only numbers (Terry Yiu)
[1.1.0-3]: https://github.com/damus-io/damus/releases/tag/v1.1.0-3
## [1.1.0-2] - 2023-02-14
### Added
- Save drafts to posts, replies and DMs (Terry Yiu)
### Fixed
- Ensure stats get updated in realtime on action bars (William Casarin)
- Fix reposts not getting counted properly (William Casarin)
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
- Fix punctuation getting included in some urls (Gert Goet)
- Improve language detection (Terry Yiu)
- Fix some animated image crashes (William Casarin)
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
## [1.0.0-15] - 2023-02-10
### Added
@@ -559,3 +610,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
-18
View File
@@ -108,24 +108,6 @@ All user-facing strings must have a comment in order to provide context to trans
[transifex]: https://explore.transifex.com/damus/damus-ios/
#### Export Source Translations
If user-facing strings have been added or changed, please export them for translation as part of your pull request or commit by running:
```zsh
./devtools/export-source-translation.sh
```
This command will export source translations to `translations/en-US.xcloc/Localized Contents/en-US.xliff`, which the Transifex integration will read from the `master` branch and allow translators to translate those strings.
#### Import Translations
Once 100% of strings have been translated for a given locale, Transifex will open up a pull request with the `translations/<locale>.xliff` file changed. Currently, it must be manually imported into the project before merging the pull request by running:
```zsh
./devtools/import-translation.sh <locale_code_in_snake_case>
```
### Awards
There may be nostr badges awarded for contributors in the future... :)
+63 -10
View File
@@ -42,6 +42,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; };
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -155,6 +156,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.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 */; };
@@ -198,6 +202,7 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
@@ -205,9 +210,10 @@
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
@@ -217,7 +223,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -249,6 +255,9 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; 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>"; };
@@ -259,6 +268,12 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -312,6 +327,7 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; };
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -455,6 +471,9 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@@ -501,15 +520,17 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.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>"; };
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>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
@@ -519,7 +540,7 @@
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>"; };
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>"; };
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -655,7 +676,6 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
@@ -670,6 +690,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
@@ -708,7 +729,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -780,6 +801,9 @@
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -842,6 +866,15 @@
path = Events;
sourceTree = "<group>";
};
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup;
children = (
@@ -860,6 +893,7 @@
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -919,6 +953,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
@@ -1116,6 +1151,9 @@
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1207,9 +1245,11 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1246,7 +1286,6 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1254,6 +1293,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
@@ -1277,6 +1317,7 @@
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
@@ -1327,6 +1368,7 @@
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -1342,6 +1384,7 @@
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
@@ -1374,6 +1417,7 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
@@ -1421,6 +1465,9 @@
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1441,6 +1488,9 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1461,6 +1511,9 @@
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1596,7 +1649,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1638,7 +1691,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-copy.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-key.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

@@ -1,52 +0,0 @@
{
"images" : [
{
"filename" : "ic-message-black.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "ic-message-white 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-nipverified.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-qr.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "profile-banner.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "bbw.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "bitcoin-p2p.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "blixt-wallet.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "bluewallet.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "breez.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "cashapp.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "digital-nomad.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "encrypted-message.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-lightning.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

-21
View File
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-tick.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 B

+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "lnlink.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "damus-nobg.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "muun.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "phoenix.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "river.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "strike.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "undercover.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "walletofsatoshi.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "zebedee.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "zeus.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+11 -45
View File
@@ -66,20 +66,11 @@ struct ImageContextMenuModifier: ViewModifier {
private struct ImageContainerView: View {
@ObservedObject var imageModel: KFImageModel
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
downsampleSize: CGSize(width: 400, height: 400)
)
}
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -91,30 +82,17 @@ private struct ImageContainerView: View {
var body: some View {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
KFAnimatedImage(url)
.imageContext(.note)
.configure { view in
view.framePreloadCount = 1
view.framePreloadCount = 3
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
ShareSheet(activityItems: [url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
@@ -127,14 +105,6 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
var navBarView: some View {
VStack {
HStack {
@@ -180,8 +150,8 @@ struct ImageView: View {
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
@@ -210,7 +180,7 @@ struct ImageView: View {
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
)
}
}
@@ -229,12 +199,8 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
+96
View File
@@ -0,0 +1,96 @@
//
// SelectableText.swift
// damus
//
// Created by Oleg Abalonski on 2/16/23.
//
import UIKit
import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.onAppear {
self.selectedTextWidth = geo.size.width
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width
}
}
.frame(height: selectedTextHeight)
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
DispatchQueue.main.async {
height = newHeight
}
}
func createNSAttributedString() -> NSMutableAttributedString {
let mutableAttributedString = NSMutableAttributedString(attributedString)
let myAttribute = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: textColor
]
mutableAttributedString.addAttributes(
myAttribute,
range: NSRange.init(location: 0, length: mutableAttributedString.length)
)
return mutableAttributedString
}
}
fileprivate extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.size.height)
}
}
+2 -5
View File
@@ -11,7 +11,6 @@ import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en"
@@ -34,9 +33,7 @@ struct TranslateView: View {
}
.translate_button_style()
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
SelectableText(attributedString: artifacts.content)
}
}
@@ -143,6 +140,6 @@ struct TranslateView: View {
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .selected)
TranslateView(damus_state: ds, event: test_event)
}
}
+2 -3
View File
@@ -100,7 +100,7 @@ struct ZapButton: View {
}
var body: some View {
ZStack {
HStack(spacing: 4) {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
@@ -110,8 +110,7 @@ struct ZapButton: View {
}
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
.offset(x: 22)
Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")"))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
+7 -7
View File
@@ -92,7 +92,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -136,7 +136,7 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
}
}
@@ -163,7 +163,7 @@ struct ContentView: View {
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
@@ -192,7 +192,7 @@ struct ContentView: View {
case .notifications:
VStack(spacing: 0) {
Divider()
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
}
case .dms:
DirectMessagesView(damus_state: damus_state!)
@@ -202,7 +202,7 @@ struct ContentView: View {
EmptyView()
}
}
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Universe 🛸", comment: "Navigation bar title for universal view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
timelineNavItem
@@ -295,7 +295,7 @@ struct ContentView: View {
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label("Filter", systemImage: "line.3.horizontal.decrease")
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
@@ -328,7 +328,7 @@ struct ContentView: View {
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
case .event(let event):
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
+56 -2
View File
@@ -9,9 +9,63 @@ import Foundation
class EventsModel: ObservableObject {
var has_event: Set<String> = Set()
let state: DamusState
let target: String
let kind: NostrKind
let sub_id = UUID().uuidString
let profiles_id = UUID().uuidString
@Published var events: [NostrEvent] = []
init() {
init(state: DamusState, target: String, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([kind.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
state.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
return
}
guard last_etag(tags: ev.tags) == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state)
}
}
}
+81 -31
View File
@@ -38,6 +38,9 @@ class HomeModel: ObservableObject {
var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false
var incoming_dms: [NostrEvent] = []
let dm_debouncer = Debouncer(interval: 0.5)
var should_debounce_dms = true
let home_subid = UUID().description
let contacts_subid = UUID().description
@@ -47,25 +50,37 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = []
@Published var notifications: EventHolder
@Published var dms: DirectMessagesModel
@Published var events: [NostrEvent] = []
@Published var events: EventHolder
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.dms = DirectMessagesModel(our_pubkey: "")
}
init(damus_state: DamusState) {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
@@ -128,7 +143,7 @@ class HomeModel: ObservableObject {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
if !notifications.insert(ev) {
return
}
@@ -180,9 +195,9 @@ class HomeModel: ObservableObject {
}
func filter_muted() {
self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) }
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
}
func handle_delete_event(_ ev: NostrEvent) {
@@ -230,6 +245,7 @@ class HomeModel: ObservableObject {
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted)
notify(.update_stats, e)
}
}
@@ -247,6 +263,7 @@ class HomeModel: ObservableObject {
case .success(let n):
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
}
}
@@ -287,7 +304,7 @@ class HomeModel: ObservableObject {
switch ev {
case .event(let sub_id, let ev):
// globally handle likes
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return
@@ -301,10 +318,11 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
let dms = dms.dms.flatMap { $0.1.events }
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state)
}
self.loading = false
@@ -443,10 +461,10 @@ class HomeModel: ObservableObject {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
if !notifications.insert(ev) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
}
@@ -456,12 +474,10 @@ class HomeModel: ObservableObject {
}
}
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
if ok {
func insert_home_event(_ ev: NostrEvent) {
if events.insert(ev) {
handle_last_event(ev: ev, timeline: .home)
}
return ok
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
@@ -470,15 +486,33 @@ class HomeModel: ObservableObject {
}
if sub_id == home_subid {
let _ = insert_home_event(ev)
insert_home_event(ev)
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
if let notifs = handle_incoming_dm(contacts: damus_state.contacts, prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
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
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
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
}
self.incoming_dms = []
}
}
}
@@ -624,14 +658,14 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if let _ = URL(string: picture) {
if URL(string: picture) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
let banner = tprof.profile.banner ?? ""
if let _ = URL(string: banner) {
if URL(string: banner) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
@@ -759,14 +793,11 @@ func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
func process_relay_metadata() {
}
func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
// hide blocked users
guard should_show_event(contacts: contacts, ev: ev) else {
return prev_events
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
@@ -793,15 +824,34 @@ func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: Dir
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
inserted = true
}
var new_bits: NewEventsBits? = nil
if inserted {
new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
}
return (inserted, new_bits)
}
@discardableResult
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
var inserted = false
var new_events: NewEventsBits? = nil
for ev in evs {
let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
inserted = res.0 || inserted
if let new = res.1 {
new_events = new
}
}
if inserted {
new_events = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
-114
View File
@@ -1,114 +0,0 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import UIKit
import Kingfisher
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
+27 -1
View File
@@ -211,6 +211,32 @@ enum Amount: Equatable {
}
}
func format_actions_abbrev(_ actions: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
if actions >= 1_000_000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if actions >= 1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return "\(actions)"
}
let actions = NSNumber(value: actions)
return formatter.string(from: actions) ?? "\(actions)"
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
@@ -247,7 +273,7 @@ func format_msats(_ msat: Int64) -> String {
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
return String(format: Bundle.main.localizedString(forKey: "sats_count", value: nil, table: nil), sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
+4 -2
View File
@@ -8,7 +8,7 @@
import Foundation
class ProfileModel: ObservableObject, Equatable {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil
@@ -111,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable {
return
}
if ev.is_textlike || ev.known_kind == .boost {
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
if self.events.insert(ev) {
self.objectWillChange.send()
}
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
+3 -65
View File
@@ -8,71 +8,9 @@
import Foundation
class ReactionsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
final class ReactionsModel: EventsModel {
@Published var reactions: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reactions = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([7])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == 7 else {
return
}
guard let reacted_to = last_etag(tags: ev.tags) else {
return
}
guard reacted_to == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reactions, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reactions, damus_state: state)
break
}
init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .like)
}
}
+4 -66
View File
@@ -7,71 +7,9 @@
import Foundation
class RepostsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
@Published var reposts: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reposts = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([NostrKind.boost.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == NostrKind.boost.rawValue else {
return
}
guard let reposted_event = last_etag(tags: ev.tags) else {
return
}
guard reposted_event == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reposts, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reposts, damus_state: state)
break
}
final class RepostsModel: EventsModel {
init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .boost)
}
}
+6 -5
View File
@@ -10,7 +10,7 @@ import Foundation
/// The data model for the SearchHome view, typically something global-like
class SearchHomeModel: ObservableObject {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
var seen_pubkey: Set<String> = Set()
@@ -31,7 +31,8 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
@@ -61,8 +62,8 @@ class SearchHomeModel: ObservableObject {
}
seen_pubkey.insert(ev.pubkey)
let _ = insert_uniq_sorted_event(events: &events, new_ev: ev) {
$0.created_at > $1.created_at
if self.events.insert(ev) {
self.objectWillChange.send()
}
}
case .notice(let msg):
@@ -75,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime
unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state)
}
+4 -3
View File
@@ -9,7 +9,7 @@ import Foundation
class SearchModel: ObservableObject {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
@Published var channel_name: String? = nil
@@ -26,7 +26,8 @@ class SearchModel: ObservableObject {
}
func filter_muted() {
self.events = self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
@@ -57,7 +58,7 @@ class SearchModel: ObservableObject {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
if self.events.insert(ev) {
objectWillChange.send()
}
}
+2 -16
View File
@@ -278,21 +278,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return (self.flags & 1) != 0
}
init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
self.id = ""
self.sig = ""
self.content = content
self.pubkey = pubkey
self.kind = kind
self.tags = tags
self.created_at = createdAt
}
/// Intiialization statement used to specificy ID
///
/// This is mainly used for contant and testing data
init(id: String, content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) {
init(id: String = "", content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
self.id = id
self.sig = ""
@@ -300,7 +286,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
self.pubkey = pubkey
self.kind = kind
self.tags = tags
self.created_at = Int64(Date().timeIntervalSince1970)
self.created_at = createdAt
}
init(from: NostrEvent, content: String? = nil) {
+1 -1
View File
@@ -72,7 +72,7 @@ func char_to_hex(_ c: UInt8) -> UInt8?
return nil;
}
@discardableResult
func hex_decode(_ str: String) -> [UInt8]?
{
if str.count == 0 {
+40 -37
View File
@@ -13,42 +13,41 @@ enum NostrConnectionEvent {
case nostr_event(NostrResponse)
}
class RelayConnection: WebSocketDelegate {
var isConnected: Bool = false
var isConnecting: Bool = false
var isReconnecting: Bool = false
var last_connection_attempt: Double = 0
var socket: WebSocket
var handleEvent: (NostrConnectionEvent) -> ()
let url: URL
final class RelayConnection: WebSocketDelegate {
private(set) var isConnected = false
private(set) var isConnecting = false
private(set) var isReconnecting = false
private(set) var last_connection_attempt: TimeInterval = 0
private lazy var socket = {
let req = URLRequest(url: url)
let socket = WebSocket(request: req, compressionHandler: .none)
socket.delegate = self
return socket
}()
private var handleEvent: (NostrConnectionEvent) -> ()
private let url: URL
init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
self.url = url
self.handleEvent = handleEvent
// just init, we don't actually use this one
self.socket = make_websocket(url: url)
}
func reconnect() {
if self.isConnected {
self.isReconnecting = true
self.disconnect()
if isConnected {
isReconnecting = true
disconnect()
} else {
// we're already disconnected, so just connect
self.connect(force: true)
connect(force: true)
}
}
func connect(force: Bool = false){
if !force && (self.isConnected || self.isConnecting) {
func connect(force: Bool = false) {
if !force && (isConnected || isConnecting) {
return
}
var req = URLRequest(url: self.url)
req.timeoutInterval = 5
socket = make_websocket(url: url)
socket.delegate = self
isConnecting = true
last_connection_attempt = Date().timeIntervalSince1970
socket.connect()
@@ -68,7 +67,9 @@ class RelayConnection: WebSocketDelegate {
socket.write(string: req)
}
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected:
@@ -83,15 +84,25 @@ class RelayConnection: WebSocketDelegate {
self.connect()
}
case .cancelled: fallthrough
case .error:
case .cancelled, .error:
self.isConnecting = false
self.isConnected = false
case .text(let txt):
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
if txt.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
return
}
}
} else {
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
}
}
print("decode failed for \(txt)")
@@ -103,7 +114,6 @@ class RelayConnection: WebSocketDelegate {
handleEvent(.ws_event(event))
}
}
func make_nostr_req(_ req: NostrRequest) -> String? {
@@ -127,7 +137,7 @@ func make_nostr_push_event(ev: NostrEvent) -> String? {
}
func make_nostr_unsubscribe_req(_ sub_id: String) -> String? {
return "[\"CLOSE\",\"\(sub_id)\"]"
"[\"CLOSE\",\"\(sub_id)\"]"
}
func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? {
@@ -144,10 +154,3 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
req += "]"
return req
}
func make_websocket(url: URL) -> WebSocket {
let req = URLRequest(url: url)
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
return WebSocket(request: req, compressionHandler: .none)
}
+4 -18
View File
@@ -195,27 +195,13 @@ class RelayPool {
relay.connection.send(req)
}
}
func get_relays(_ ids: [String]) -> [Relay] {
var relays: [Relay] = []
for id in ids {
if let relay = get_relay(id) {
relays.append(relay)
}
}
return relays
relays.filter { ids.contains($0.id) }
}
func get_relay(_ id: String) -> Relay? {
for relay in relays {
if relay.id == id {
return relay
}
}
return nil
relays.first(where: { $0.id == id })
}
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
+1
View File
@@ -143,6 +143,7 @@ func eightToFiveBits(_ input: [UInt8]) -> [UInt8] {
}
/// Decode Bech32 string
@discardableResult
public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
guard let strBytes = str.data(using: .utf8) else {
throw Bech32Error.nonUTF8String
+27
View File
@@ -0,0 +1,27 @@
//
// Debouncer.swift
// damus
//
// Created by William Casarin on 2023-02-15.
//
import Foundation
class Debouncer {
private let queue = DispatchQueue.main
private var workItem: DispatchWorkItem?
private var interval: TimeInterval
init(interval: TimeInterval) {
self.interval = interval
}
func debounce(action: @escaping () -> Void) {
// Cancel the previous work item if it hasn't yet executed
workItem?.cancel()
// Create a new work item with a delay
workItem = DispatchWorkItem { action() }
queue.asyncAfter(deadline: .now() + interval, execute: workItem!)
}
}
+99
View File
@@ -0,0 +1,99 @@
//
// EventHolder.swift
// damus
//
// Created by William Casarin on 2023-02-19.
//
import Foundation
/// Used for holding back events until they're ready to be displayed
class EventHolder: ObservableObject {
private var has_event: Set<String>
@Published var events: [NostrEvent]
@Published var incoming: [NostrEvent]
@Published var should_queue: Bool
var queued: Int {
return incoming.count
}
var has_incoming: Bool {
return queued > 0
}
var all_events: [NostrEvent] {
events + incoming
}
init() {
self.should_queue = false
self.events = []
self.incoming = []
self.has_event = Set()
}
init(events: [NostrEvent], incoming: [NostrEvent]) {
self.should_queue = false
self.events = events
self.incoming = incoming
self.has_event = Set()
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
self.events = self.events.filter(isIncluded)
self.incoming = self.incoming.filter(isIncluded)
}
func insert(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_queued(ev)
} else {
return insert_immediate(ev)
}
}
private func insert_immediate(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) {
return true
}
return false
}
private func insert_queued(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
incoming.append(ev)
return true
}
func flush() {
guard !incoming.isEmpty else {
return
}
var changed = false
for event in incoming {
if insert_uniq_sorted_event_created(events: &events, new_ev: event) {
changed = true
}
}
if changed {
self.objectWillChange.send()
}
self.incoming = []
}
}
+9
View File
@@ -38,6 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true
}
@discardableResult
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
var i: Int = 0
@@ -58,6 +59,14 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return true
}
func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event(events: &events, new_ev: new_ev) {
$0.created_at > $1.created_at
}
}
@discardableResult
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0
+148
View File
@@ -0,0 +1,148 @@
//
// KFOptionSetter+.swift
// damus
//
// Created by Oleg Abalonski on 2/15/23.
//
import UIKit
import Kingfisher
extension KFOptionSetter {
func imageContext(_ imageContext: ImageContext) -> Self {
options.callbackQueue = .dispatch(.global(qos: .background))
options.processingQueue = .dispatch(.global(qos: .background))
options.downloader = CustomImageDownloader.shared
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.processor = CustomImageProcessor(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.cacheSerializer = CustomCacheSerializer(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
return self
}
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]
return self
}
}
let MAX_FILE_SIZE = 20_971_520 // 20MiB
enum ImageContext {
case pfp
case banner
case note
func maxMebibyteSize() -> Int {
switch self {
case .pfp:
return 5_242_880 // 5Mib
case .banner, .note:
return 20_971_520 // 20MiB
}
}
func downsampleSize() -> CGSize {
switch self {
case .pfp:
return CGSize(width: 200, height: 200)
case .banner:
return CGSize(width: 750, height: 250)
case .note:
return CGSize(width: 500, height: 500)
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let contentLength = response.expectedContentLength
// Content-Length header is optional (-1 when missing)
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
}
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
}
}
class CustomImageDownloader: ImageDownloader {
static let shared = CustomImageDownloader(name: "shared")
override init(name: String) {
super.init(name: name)
sessionDelegate = CustomSessionDelegate()
}
}
+5 -2
View File
@@ -8,8 +8,8 @@
import Foundation
public struct Markdown {
private let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
private var detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
/// Ensure the specified URL has a scheme by prepending "https://" if it's absent.
static func withScheme(_ url: any StringProtocol) -> any StringProtocol {
return url.contains("://") ? url : "https://" + url
@@ -31,6 +31,9 @@ public struct Markdown {
/// Process the input text and add markdown for any embedded URLs.
public func process(_ input: String) -> AttributedString {
guard let detector else {
return AttributedString(stringLiteral: input)
}
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
var output = input
// Start with the last match, because replacing the first would invalidate all subsequent indices
+2 -2
View File
@@ -98,8 +98,8 @@ extension Notification.Name {
static var deleted_account: Notification.Name {
return Notification.Name("deleted_account")
}
static var new_zap: Notification.Name {
return Notification.Name("new_zap")
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
}
+8
View File
@@ -25,4 +25,12 @@ class Theme {
UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black
}
static var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
}
+2 -4
View File
@@ -36,7 +36,7 @@ class Zaps {
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
} else {
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
}
case .profile(_):
break
@@ -60,8 +60,6 @@ class Zaps {
event_counts[id] = event_counts[id]! + 1
event_totals[id] = event_totals[id]! + zap.invoice.amount
notify(.new_zap, zap)
return
notify(.update_stats, zap.target.id)
}
}
+16 -17
View File
@@ -51,7 +51,7 @@ struct EventActionBar: View {
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
}
Spacer()
ZStack {
HStack(spacing: 4) {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
@@ -61,14 +61,13 @@ struct EventActionBar: View {
}
}
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 18)
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Spacer()
ZStack {
HStack(spacing: 4) {
LikeButton(liked: bar.liked) {
if bar.liked {
notify(.delete, bar.our_like)
@@ -76,8 +75,7 @@ struct EventActionBar: View {
send_like()
}
}
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
.offset(x: 22)
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
@@ -111,15 +109,10 @@ struct EventActionBar: View {
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
.onReceive(handle_notify(.new_zap)) { n in
let zap = n.object as! Zap
guard case .note(let note_target) = zap.target else {
return
}
guard note_target.note_id == self.event.id else {
return
}
self.bar.update(damus: self.damus_state, evid: self.event.id)
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
self.bar.update(damus: self.damus_state, evid: target)
}
.onReceive(handle_notify(.liked)) { n in
let liked = n.object as! Counted
@@ -163,9 +156,9 @@ struct EventActionBar: View {
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
Button(action: action) {
Label(NSLocalizedString("\u{00A0}", comment: "Non-breaking space character to fill in blank space next to event action button icons."), systemImage: img)
.font(.footnote.weight(.medium))
Image(systemName: img)
.foregroundColor(col == nil ? Color.gray : col!)
.font(.footnote.weight(.medium))
}
}
@@ -195,6 +188,8 @@ struct EventActionBar_Previews: PreviewProvider {
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) {
@@ -205,7 +200,11 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
}
.padding(20)
+3 -3
View File
@@ -26,14 +26,14 @@ struct EventDetailBar: View {
HStack {
if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text("\(bar.boosts)", comment: "Number of reposts.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reposts_count", comment: "Part of a larger sentence to describe how many reposts there are."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
Text("\(Text("\(bar.likes)", comment: "Number of reactions on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reactions_count", comment: "Part of a larger sentence to describe how many reactions there are on a post."), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -41,7 +41,7 @@ struct EventDetailBar: View {
if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
.buttonStyle(PlainButtonStyle())
}
+5 -22
View File
@@ -10,40 +10,23 @@ import Kingfisher
struct InnerBannerImageView: View {
let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@ObservedObject var imageModel: KFImageModel
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 20_971_520, // 20 MiB
downsampleSize: CGSize(width: 750, height: 250)
)
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
if (imageModel.url != nil) {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
if (url != nil) {
KFAnimatedImage(url)
.imageContext(.banner)
.configure { view in
view.framePreloadCount = 1
view.framePreloadCount = 3
}
.placeholder { _ in
Color(uiColor: .secondarySystemBackground)
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailureImage(defaultImage)
.id(imageModel.refreshID)
} else {
Image(uiImage: defaultImage).resizable()
}
+5 -5
View File
@@ -63,7 +63,7 @@ struct ChatView: View {
}
var ReplyDescription: some View {
Text("\(reply_desc(profiles: damus_state.profiles, event: event))")
Text(verbatim: "\(reply_desc(profiles: damus_state.profiles, event: event))")
.font(.footnote)
.foregroundColor(.gray)
.frame(alignment: .leading)
@@ -89,12 +89,12 @@ struct ChatView: View {
ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), damus: damus_state, show_friend_confirmed: true)
.foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black)
//.shadow(color: Color.black, radius: 2)
Text("\(format_relative_time(event.created_at))")
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
}
}
if let ref_id = thread.replies.lookup(event.id) {
if let _ = thread.replies.lookup(event.id) {
if !is_reply_to_prev() {
/*
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
@@ -112,8 +112,8 @@ struct ChatView: View {
NoteContentView(damus_state: damus_state,
event: event,
show_images: show_images,
artifacts: .just_content(event.content),
size: .normal)
size: .normal,
artifacts: .just_content(event.content))
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
+21 -8
View File
@@ -8,6 +8,7 @@ import AVFoundation
import Kingfisher
import SwiftUI
import LocalAuthentication
import Combine
struct ConfigView: View {
let state: DamusState
@@ -130,7 +131,25 @@ struct ConfigView: View {
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField("1000", text: $default_zap_amount)
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
let filtered = newValue.filter { Set("0123456789").contains($0) }
if filtered != newValue {
default_zap_amount = filtered
}
if filtered == "" {
set_default_zap_amount(pubkey: state.pubkey, amount: 1000)
return
}
guard let amt = Int(filtered) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
@@ -204,16 +223,10 @@ struct ConfigView: View {
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text("\(bundleShortVersion) (\(bundleVersion))", comment: "Text indicating which version of the Damus app is running. Should typically not need to be translated.")
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
}
}
}
.onChange(of: default_zap_amount) { val in
guard let amt = Int(val) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
+2 -2
View File
@@ -36,14 +36,14 @@ struct CreateAccountView: View {
HStack(alignment: .top) {
VStack {
Text(" ", comment: "Blank space to separate profile picture from profile editor form.")
Text(verbatim: " ")
.foregroundColor(.white)
}
VStack {
SignupForm {
FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry."))
HStack(spacing: 0.0) {
Text("@", comment: "Prefix character to username.")
Text(verbatim: "@")
.foregroundColor(.white)
.padding(.leading, -25.0)
+1 -1
View File
@@ -23,7 +23,7 @@ struct DMView: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)))
.foregroundColor(is_ours ? Color.white : Color.primary)
.padding(10)
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
+2 -1
View File
@@ -33,13 +33,14 @@ struct DirectMessagesView: View {
NavigationLink(destination: chat, isActive: $open_dm) {
EmptyView()
}
LazyVStack {
LazyVStack(spacing: 0) {
if model.dms.isEmpty, !model.loading {
EmptyTimelineView()
} else {
let dms = requests ? model.message_requests : model.friend_dms
ForEach(dms, id: \.0) { tup in
MaybeEvent(tup)
.padding(.top, 10)
}
}
}
+4 -4
View File
@@ -10,7 +10,7 @@ import SwiftUI
struct EventDetailView: View {
var body: some View {
Text("EventDetailView")
Text(verbatim: "EventDetailView")
}
}
@@ -33,14 +33,14 @@ func print_event(_ ev: NostrEvent) {
print(ev.description)
}
func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool) {
func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool, anchor: UnitPoint = .bottom) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if animate {
withAnimation {
scroller.scrollTo(id, anchor: .bottom)
scroller.scrollTo(id, anchor: anchor)
}
} else {
scroller.scrollTo(id, anchor: .bottom)
scroller.scrollTo(id, anchor: anchor)
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ struct EventBody: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(content), size: size)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content))
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+13 -9
View File
@@ -13,7 +13,7 @@ struct ReplyDescription: View {
let profiles: Profiles
var body: some View {
Text("\(reply_desc(profiles: profiles, event: event))")
Text(verbatim: "\(reply_desc(profiles: profiles, event: event))")
.font(.footnote)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -32,7 +32,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
let n = desc.others
if desc.pubkeys.count == 0 {
return NSLocalizedString("Reply to self", comment: "Label to indicate that the user is replying to themself.")
return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.")
}
let names: [String] = pubkeys.map {
@@ -40,16 +40,20 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
return Profile.displayName(profile: prof, pubkey: $0)
}
if names.count == 2 {
if n > 2 {
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_two_and_others", comment: "Label to indicate that the user is replying to 2 users and others."), names[0], names[1], othersCount)
let othersCount = n - pubkeys.count
if names.count > 1 {
if othersCount == 0 {
return String(format: "Replying to %@ & %@", names[0], names[1])
} else {
return String(format: "Replying to %@, %@ & %d others", names[0], names[1], othersCount)
}
return String(format: NSLocalizedString("Replying to %@ & %@", comment: "Label to indicate that the user is replying to 2 users."), names[0], names[1])
}
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_one_and_others", comment: "Label to indicate that the user is replying to 1 user and others."), names[0], othersCount)
if othersCount == 0 {
return String(format: "Replying to %@", names[0])
} else {
return String(format: "Replying to %@ & %d others", names[0], othersCount)
}
}
+5 -10
View File
@@ -35,7 +35,7 @@ struct SelectedEventView: View {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
Text("\(format_date(event.created_at))")
Text(verbatim: "\(format_date(event.created_at))")
.padding(.top, 10)
.font(.footnote)
.foregroundColor(.gray)
@@ -54,15 +54,10 @@ struct SelectedEventView: View {
Divider()
.padding([.top], 4)
}
.onReceive(handle_notify(.new_zap)) { n in
let zap = n.object as! Zap
guard case .note(let note_target) = zap.target else {
return
}
guard note_target.note_id == self.event.id else {
return
}
self.bar.update(damus: self.damus, evid: self.event.id)
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target)
}
.padding([.leading], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
+1 -1
View File
@@ -33,7 +33,7 @@ struct TextEvent: View {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text("\(format_relative_time(event.created_at))")
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
Spacer()
+5 -5
View File
@@ -70,19 +70,19 @@ struct FollowButtonPreviews: View {
let target: FollowTarget = .pubkey("")
var body: some View {
VStack {
Text("Unfollows", comment: "Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.")
Text(verbatim: "Unfollows")
FollowButtonView(target: target, follows_you: false, follow_state: .unfollows)
Text("Following", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.")
Text(verbatim: "Following")
FollowButtonView(target: target, follows_you: false, follow_state: .following)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
Text(verbatim: "Follows")
FollowButtonView(target: target, follows_you: false, follow_state: .follows)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
Text(verbatim: "Follows")
FollowButtonView(target: target, follows_you: true, follow_state: .follows)
Text("Unfollowing", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.")
Text(verbatim: "Unfollowing")
FollowButtonView(target: target, follows_you: false, follow_state: .unfollowing)
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ struct MentionView: View {
let pk = bech32_pubkey(mention.ref.ref_id) ?? mention.ref.ref_id
PubkeyView(pubkey: pk, relay: mention.ref.relay_id)
case .event:
Text("< e >", comment: "Placeholder for event mention.")
Text(verbatim: "< e >")
//EventBlockView(pubkey: mention.ref.ref_id, relay: mention.ref.relay_id)
}
}
+28 -16
View File
@@ -9,37 +9,49 @@ import SwiftUI
import LinkPresentation
import NaturalLanguage
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemUltraThinMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
struct NoteContentView: View {
let damus_state: DamusState
let event: NostrEvent
let show_images: Bool
let size: EventViewKind
@State var artifacts: NoteArtifacts
let size: EventViewKind
@State var preview: LinkViewRepresentable? = nil
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
if size == .selected {
TranslateView(damus_state: damus_state, event: event, size: size)
SelectableText(attributedString: artifacts.content)
TranslateView(damus_state: damus_state, event: event)
} else {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
.blur(radius: 10)
.overlay {
Rectangle()
.opacity(0.50)
}
.cornerRadius(10)
ZStack {
ImageCarousel(urls: artifacts.images)
Blur()
.disabled(true)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
@@ -155,7 +167,7 @@ struct NoteContentView_Previews: PreviewProvider {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, artifacts: artifacts, size: .normal)
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts)
}
}
-79
View File
@@ -1,79 +0,0 @@
//
// ParicipantsView.swift
// damus
//
// Created by Joel Klabo on 1/18/23.
//
import SwiftUI
struct ParticipantsView: View {
let damus_state: DamusState
@Binding var references: [ReferencedId]
@Binding var originalReferences: [ReferencedId]
var body: some View {
VStack {
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.")
}
.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")
}
.buttonStyle(.borderedProminent)
Spacer()
}
VStack {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
} else {
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
}
}
}
}
Spacer()
}
.padding()
}
}
+81
View File
@@ -0,0 +1,81 @@
//
// ParicipantsView.swift
// damus
//
// Created by Joel Klabo on 1/18/23.
//
import SwiftUI
struct ParticipantsView: View {
let damus_state: DamusState
@Binding var references: [ReferencedId]
@Binding var originalReferences: [ReferencedId]
var body: some View {
VStack {
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.")
}
.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")
}
.buttonStyle(.borderedProminent)
Spacer()
}
VStack {
ScrollView {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
} else {
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
}
}
}
}
}
Spacer()
}
.padding()
}
}
+37 -17
View File
@@ -80,28 +80,42 @@ struct PostView: View {
self.send_post()
}
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
}
}
.frame(height: 30)
.padding([.top, .bottom], 4)
ZStack(alignment: .topLeading) {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
if let replying_to {
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: 45.0, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
ZStack(alignment: .topLeading) {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
if let replying_to {
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
}
}
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
.padding(.leading, 4)
.foregroundColor(Color(uiColor: .placeholderText))
.allowsHitTesting(false)
}
}
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
.padding(.leading, 4)
.foregroundColor(Color(uiColor: .placeholderText))
.allowsHitTesting(false)
}
}
@@ -168,3 +182,9 @@ func get_searching_string(_ post: String) -> String? {
return String(last_word.dropFirst())
}
struct PostView_Previews: PreviewProvider {
static var previews: some View {
PostView(replying_to: nil, references: [], damus_state: test_damus_state())
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import SwiftUI
func PowView(_ mpow: Int?) -> some View
{
let pow = mpow ?? 0
return Text("\(pow)")
return Text(verbatim: "\(pow)")
.font(.callout)
.foregroundColor(calculate_pow_color(pow))
}
+2 -1
View File
@@ -18,7 +18,7 @@ struct ProfileNameView: View {
var body: some View {
Group {
if let real_name = profile?.display_name {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 0) {
Text(real_name)
.font(.title3.weight(.bold))
HStack(alignment: .center, spacing: spacing) {
@@ -30,6 +30,7 @@ struct ProfileNameView: View {
FollowsYou()
}
}
Spacer()
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
+8 -28
View File
@@ -33,23 +33,12 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
}
struct InnerProfilePicView: View {
let url: URL?
let fallbackUrl: URL?
let pubkey: String
let size: CGFloat
let highlight: Highlight
@ObservedObject var imageModel: KFImageModel
init(url: URL?, fallbackUrl: URL?, pubkey: String, size: CGFloat, highlight: Highlight) {
self.pubkey = pubkey
self.size = size
self.highlight = highlight
self.imageModel = KFImageModel(
url: url,
fallbackUrl: fallbackUrl,
maxByteSize: 5_242_880, // 5Mib
downsampleSize: CGSize(width: 200, height: 200)
)
}
var PlaceholderColor: Color {
return id_to_color(pubkey)
@@ -67,25 +56,16 @@ struct InnerProfilePicView: View {
ZStack {
Color(uiColor: .systemBackground)
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
.cacheOriginalImage()
KFAnimatedImage(url)
.imageContext(.pfp)
.onFailure(fallbackUrl: fallbackUrl, cacheKey: url?.absoluteString)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 1
view.framePreloadCount = 3
}
.placeholder { _ in
Placeholder
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
}
.frame(width: size, height: size)
.clipShape(Circle())
+246 -197
View File
@@ -80,9 +80,24 @@ struct EditButton: View {
}
}
struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
UIVisualEffectView()
}
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
uiView.effect = effect
}
}
struct ProfileView: View {
let damus_state: DamusState
let zoom_size: CGFloat = 350
let pfp_size: CGFloat = 90.0
let bannerHeight: CGFloat = 150.0
static let markdown = Markdown()
@State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel
@@ -92,20 +107,13 @@ struct ProfileView: View {
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) var openURL
// We just want to have a white "< Home" text here, however,
// setting the initialiser is causing issues, and it's late.
// Ref: https://blog.techchee.com/navigation-bar-title-style-color-and-custom-back-button-in-swiftui/
/*
init(damus_state: DamusState, zoom_size: CGFloat = 350) {
self.damus_state = damus_state
self.zoom_size = zoom_size
Theme.navigationBarColors(background: nil, titleColor: .white, tintColor: nil)
}*/
@Environment(\.presentationMode) var presentationMode
func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
@@ -115,7 +123,98 @@ struct ProfileView: View {
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
}
func LNButton(lnurl: String, profile: Profile) -> some View {
func bannerBlurViewOpacity() -> Double {
let progress = -(yOffset + navbarHeight) / 100
return Double(-yOffset > navbarHeight ? progress : 0)
}
var bannerSection: some View {
GeometryReader { proxy -> AnyView in
let minY = proxy.frame(in: .global).minY
DispatchQueue.main.async {
self.yOffset = minY
}
return AnyView(
VStack(spacing: 0) {
ZStack {
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped()
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
}
Divider().opacity(bannerBlurViewOpacity())
}
.frame(height: minY > 0 ? bannerHeight + minY : nil)
.offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight)
)
}
.frame(height: bannerHeight)
}
var navbarHeight: CGFloat {
return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
}
@ViewBuilder
func navImage(systemImage: String) -> some View {
Image(systemName: systemImage)
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
var navBackButton: some View {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
navImage(systemImage: "chevron.left")
}
}
var navActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
}) {
navImage(systemImage: "ellipsis")
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var customNavbar: some View {
HStack {
navBackButton
Spacer()
navActionSheetButton
}
.padding(.top, 5)
.padding(.horizontal)
.accentColor(Color("DamusWhite"))
}
func lnButton(lnurl: String, profile: Profile) -> some View {
Button(action: {
if damus_state.settings.show_wallet_selector {
showing_select_wallet = true
@@ -139,46 +238,8 @@ struct ProfileView: View {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
}
}
static let markdown = Markdown()
var ActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
}) {
Image(systemName: "ellipsis.circle")
.profile_button_style(scheme: colorScheme)
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var ShareButton: some View {
Button(action: {
show_share_sheet = true
}) {
Image(systemName: "square.and.arrow.up.circle")
.profile_button_style(scheme: colorScheme)
}
}
var DMButton: some View {
var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
.environmentObject(dm_model)
@@ -187,44 +248,17 @@ struct ProfileView: View {
.profile_button_style(scheme: colorScheme)
}
}
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat {
geometry.frame(in: .global).minY
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = 150.0
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
func ActionSection(profile_data: Profile?) -> some View {
func actionSection(profile_data: Profile?) -> some View {
return Group {
ActionSheetButton
if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" {
LNButton(lnurl: lnurl, profile: profile)
lnButton(lnurl: lnurl, profile: profile)
}
}
DMButton
dmButton
if profile.pubkey != damus_state.pubkey {
FollowButtonView(
@@ -241,110 +275,42 @@ struct ProfileView: View {
}
}
func NameSection(profile_data: Profile?) -> some View {
func pfpOffset() -> CGFloat {
let progress = -yOffset / navbarHeight
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
return offset > 0 ? offset : 0
}
func pfpScale() -> CGFloat {
let progress = -yOffset / navbarHeight
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
return scale < 1 ? scale : 1
}
func nameSection(profile_data: Profile?) -> some View {
return Group {
HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.padding(.top, -(pfp_size / 2.0))
.offset(y: pfpOffset())
.scaleEffect(pfpScale())
.onTapGesture {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
ActionSection(profile_data: profile_data)
.offset(y: -15.0) // Increase if set a frame
actionSection(profile_data: profile_data)
}
let follows_you = profile.follows(pubkey: damus_state.pubkey)
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
//.padding(.bottom)
.padding(.top,-(pfp_size/2.0))
}
}
var pfp_size: CGFloat {
return 90.0
}
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geometry in
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry))
.clipped()
.offset(x: 0, y: self.getOffsetForHeaderImage(geometry))
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading, spacing: 8.0) {
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
NameSection(profile_data: profile_data)
Text(ProfileView.markdown.process(profile_data?.about ?? ""))
.font(.subheadline).textSelection(.enabled)
if let url = profile_data?.website_url {
WebsiteLink(url: url)
}
Divider()
HStack {
if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack {
Text("\(Text("\(profile.following)", comment: "Number of profiles a user is following.").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
}
}
.buttonStyle(PlainButtonStyle())
}
let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey)
.environmentObject(followers)
if followers.contacts != nil {
NavigationLink(destination: fview) {
FollowersCount
}
.buttonStyle(PlainButtonStyle())
} else {
FollowersCount
.onTapGesture {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
followers.contacts = []
followers.subscribe()
}
}
if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
}
.buttonStyle(PlainButtonStyle())
} else {
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
.padding(.horizontal,18)
//.offset(y:120)
.padding(.top,150)
}
}
var FollowersCount: some View {
var followersCount: some View {
HStack {
if followers.count == nil {
Image(systemName: "square.and.arrow.down")
@@ -353,24 +319,105 @@ struct ProfileView: View {
.foregroundColor(.gray)
} else {
let followerCount = followers.count!
Text("\(Text("\(followerCount)", comment: "Number of people following a user.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("followers_count", comment: "Part of a larger sentence to describe how many people are following a user."), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
Text("\(Text(verbatim: "\(followerCount)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "followers_count", value: nil, table: nil), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
}
}
}
var aboutSection: some View {
VStack(alignment: .leading, spacing: 8.0) {
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
nameSection(profile_data: profile_data)
Text(ProfileView.markdown.process(profile_data?.about ?? ""))
.font(.subheadline).textSelection(.enabled)
if let url = profile_data?.website_url {
WebsiteLink(url: url)
}
HStack {
if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack {
Text("\(Text(verbatim: "\(profile.following)").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
}
}
.buttonStyle(PlainButtonStyle())
}
let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey)
.environmentObject(followers)
if followers.contacts != nil {
NavigationLink(destination: fview) {
followersCount
}
.buttonStyle(PlainButtonStyle())
} else {
followersCount
.onTapGesture {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
followers.contacts = []
followers.subscribe()
}
}
if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text(verbatim: "\(relays.keys.count)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "relays_count", value: nil, table: nil), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
}
.buttonStyle(PlainButtonStyle())
} else {
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
.padding(.horizontal)
}
var body: some View {
VStack(alignment: .leading) {
ScrollView {
TopSection
Divider()
ScrollView(.vertical) {
VStack(spacing: 0) {
bannerSection
.zIndex(1)
InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true })
VStack() {
aboutSection
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only your posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing your posts and replies (instead of only your posts).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
if filter_state == FilterState.posts {
InnerTimelineView(events: profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts.filter)
}
if filter_state == FilterState.posts_and_replies {
InnerTimelineView(events: profile.events, damus: damus_state, show_friend_icon: false, filter: FilterState.posts_and_replies.filter)
}
}
.padding(.horizontal, Theme.safeAreaInsets?.left)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
}
.frame(maxHeight: .infinity, alignment: .topLeading)
}
.frame(maxWidth: .infinity, alignment: .topLeading)
.ignoresSafeArea()
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
@@ -390,7 +437,6 @@ struct ProfileView: View {
}
}
}
.ignoresSafeArea()
}
}
@@ -403,7 +449,6 @@ struct ProfileView_Previews: PreviewProvider {
}
}
func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState.empty
@@ -429,22 +474,30 @@ struct KeyView: View {
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
private func copyPubkey(_ pubkey: String) {
UIPasteboard.general.string = pubkey
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
withAnimation {
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
isCopied = false
}
}
}
}
var body: some View {
let bech32 = bech32_pubkey(pubkey) ?? pubkey
HStack {
RoundedRectangle(cornerRadius: 24)
.frame(width: 275, height:22)
RoundedRectangle(cornerRadius: 11)
.frame(height: 22)
.foregroundColor(fillColor())
.overlay(
HStack {
Button {
UIPasteboard.general.string = bech32
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
isCopied = false
}
copyPubkey(bech32)
} label: {
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
.font(.custom("key", size: 12.0))
@@ -456,23 +509,18 @@ struct KeyView: View {
Text(abbrev_pubkey(bech32, amount: 16))
.font(.footnote)
.foregroundColor(keyColor())
.offset(x:-3) // Not sure why this is needed.
}
)
if isCopied != true {
Button {
UIPasteboard.general.string = bech32
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
isCopied = false
}
copyPubkey(bech32)
} label: {
Label {
Text("Public key", comment: "Label indicating that the text is a user's public account key.")
} icon: {
Image("ic-copy")
Image(systemName: "square.on.square.dashed")
.contentShape(Rectangle())
.foregroundColor(.gray)
.frame(width: 20, height: 20)
}
.labelStyle(IconOnlyLabelStyle())
@@ -480,12 +528,13 @@ struct KeyView: View {
}
} else {
HStack {
Image("ic-tick")
Image(systemName: "checkmark.circle")
.frame(width: 20, height: 20)
Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied."))
.font(.footnote)
.foregroundColor(Color("DamusGreen"))
.layoutPriority(1)
}
.foregroundColor(Color("DamusGreen"))
}
}
}
+64 -67
View File
@@ -5,84 +5,81 @@
// Created by scoder1747 on 12/27/22.
//
import SwiftUI
import Kingfisher
private struct ImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
func modify(_ image: UIImage) -> UIImage {
handler = image
return image
}
}
var body: some View {
KFAnimatedImage(url)
.imageContext(.pfp)
.configure { view in
view.framePreloadCount = 3
}
.imageModifier(ImageHandler(handler: $image))
.clipShape(Circle())
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
}
}
struct ProfileZoomView: View {
@Environment(\.presentationMode) var presentationMode
let pubkey: String
let profiles: Profiles
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1
func resetStatus(){
self.offset = CGSize.zero
self.scale = 1
}
var zoomGesture: some Gesture {
MagnificationGesture()
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture {
DragGesture()
.updating($offsetState) { currentState, gestureState, _ in
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var doubleTapGesture : some Gesture {
TapGesture(count: 2).onEnded { value in
resetStatus()
@Environment(\.presentationMode) var presentationMode
var navBarView: some View {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
.frame(width: 33, height: 33)
.background(.regularMaterial)
.clipShape(Circle())
})
Spacer()
}
.padding()
}
var body: some View {
ZStack(alignment: .topLeading) {
Color("DamusDarkGrey") // Or Color("DamusBlack")
.edgesIgnoringSafeArea(.all)
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
Button {
ZoomableScrollView {
ImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
.padding(.horizontal)
}
.ignoresSafeArea()
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
VStack(alignment: .center) {
Spacer()
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles)
.padding(100)
.scaledToFit()
.scaleEffect(self.scale * scaleState)
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture)
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
Spacer()
}
}))
}
.overlay(navBarView, alignment: .top)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ struct PubkeyView: View {
var body: some View {
let color: Color = id_to_color(pubkey)
ZStack {
Text("\(abbrev_pubkey(pubkey))", comment: "Abbreviated version of a nostr public key.")
Text(verbatim: "\(abbrev_pubkey(pubkey))")
.foregroundColor(color)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ struct ReactionsView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.reactions, id: \.id) { ev in
ForEach(model.events, id: \.id) { ev in
ReactionView(damus_state: damus_state, reaction: ev)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ struct RelayFilterView: View {
}
var body: some View {
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:")
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
.padding()
.padding(.top, 20)
.padding(.bottom, 0)

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