Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a4327a6a7a
|
|||
| 502c4daf6f | |||
| ffe2c7284a | |||
| 6b1f57d6d0 | |||
| 77f5268336 | |||
| c72c0079cc | |||
| 5ab1d6294c | |||
| 2f90f2d4b7 | |||
| 1288732e5d | |||
| 4a6c6a65ab | |||
| 0f29d67e1f | |||
| 9fd2f51971 | |||
| 386bae64ca | |||
| 4b5c217213 | |||
|
240fda2429
|
|||
|
bacd9b3c38
|
|||
|
0152286859
|
|||
|
06e9a1b392
|
|||
|
483730af18
|
|||
|
23229015a6
|
|||
|
7ab95583df
|
|||
|
b7a48a24e9
|
|||
|
04028d9cff
|
|||
|
6918fb46cf
|
|||
|
2b854ef9b7
|
|||
|
5eb61f1ac1
|
|||
|
c3bbf7aa8f
|
|||
|
1e52d958ac
|
|||
|
5252e5f5bb
|
|||
|
71d5625f04
|
|||
|
990e783c30
|
|||
|
3602189133
|
|||
|
3ca9acdf34
|
|||
|
2036d5843b
|
|||
|
2d3bd11d56
|
|||
|
a715987e71
|
|||
|
0303031445
|
|||
|
d6ae9a5d79
|
|||
|
356bd06e6a
|
|||
|
e757bdca90
|
|||
|
ff1b4d724d
|
|||
|
b8614f055c
|
|||
|
63ab151a5e
|
|||
|
fd9d4deb44
|
|||
|
a5c719673e
|
|||
|
2bf3c6718d
|
|||
|
3f3e59488a
|
|||
|
b1b4b5b6c9
|
|||
|
7455665672
|
|||
|
9f22234926
|
|||
|
21a8a4e96f
|
|||
|
c635f3d77a
|
|||
|
2e9a4388b9
|
|||
|
4a1949eeb8
|
|||
|
b4fcb58bcb
|
|||
|
7d852eb33b
|
|||
|
a448f610c0
|
|||
|
8cc561b8c6
|
|||
|
01630d0a4c
|
|||
|
16156f4d9a
|
|||
|
f840fe9c80
|
|||
|
77bcd1b715
|
|||
| 75fd8de456 | |||
| 71f7ea47df | |||
| 64b1a57918 | |||
| e4dd585754 | |||
| 436d20dfbd | |||
| 810b3e1fa5 | |||
| 75fb0d19e2 | |||
| a2749eaaaa | |||
| 83c9289345 | |||
| 4c3a83772e | |||
| 5cd4c2d75e | |||
| 85e797a054 | |||
| 9f52e2c246 | |||
| 8b9958a4ad | |||
| 87a0bdac94 | |||
| 37b964c296 | |||
| b1a2b47116 | |||
| af6f88ab17 | |||
| 647495dbc0 | |||
| 826fd1ef33 | |||
| 54dd2035a1 | |||
| 587819c8eb | |||
| 8954c1c245 | |||
| 19a421604c | |||
| 68b57d8b99 | |||
| f3056653db | |||
| 6196279d2b | |||
| f213420b41 | |||
| b4140dc5f2 | |||
|
1b27e9041f
|
|||
| 795577a0a1 | |||
| d5c45dc8ba | |||
| 603a5a1814 | |||
| 06a1a9aba6 | |||
| ff1815cce0 | |||
| 0bdec912f8 | |||
| 6d8312fa57 | |||
| f6d56179eb | |||
| 193e922c9c | |||
| a1a89dc98e | |||
| 3e764e75e4 | |||
| 7c563cb0ae | |||
|
a328b0d1a8
|
|||
| 5018b9aa1e | |||
| 1f6657e471 | |||
| 062b5dc040 | |||
| 390c9162ae | |||
| 94f66adf8d | |||
| d547dade04 | |||
| 94a67adff9 | |||
| 29f192c377 | |||
|
4e67c88607
|
|||
| 42200c347b | |||
|
36f05ccaed
|
|||
|
98a1b95d12
|
|||
| 4cdef502e9 | |||
| ae2e70ba7d | |||
| 1b4e54582f | |||
| 909148f0be | |||
| c100c6db47 | |||
| 8d3fb397f7 | |||
| f8742a609c | |||
| d55d0d61ed | |||
| cf90480501 | |||
| f0075904c2 | |||
|
a41acc12e7
|
|||
| 1e22984d52 | |||
| 9080e4efae | |||
|
6488634eda
|
|||
|
355cd1283c
|
|||
| 6ed9c408f9 | |||
| 5f52e6f62f | |||
| 2366089896 | |||
| 9a95967a81 | |||
| 504108da75 | |||
| d43a2ff92d |
@@ -0,0 +1,24 @@
|
||||
name: Export Source Translations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
export-source-translations:
|
||||
name: Update translations branch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Run export script
|
||||
run: |
|
||||
sh devtools/export-source-translation.sh
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update Translations 🤖
|
||||
branch: translations
|
||||
create_branch: true
|
||||
push_options: '--force'
|
||||
if: env.GIT_DIFF
|
||||
@@ -1,3 +1,94 @@
|
||||
## [1.1.0-10] - 2023-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Truncate large posts and add a show more button (OlegAba)
|
||||
- Private Zaps (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix default zap amount setting not getting updated (William Casarin)
|
||||
- Fix issue where keyboard covers custom zap comment (William Casarin)
|
||||
|
||||
|
||||
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
|
||||
|
||||
## [1.1.0-9] - 2023-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- Customized zaps (William Casarin)
|
||||
- Add new Notifications View (William Casarin)
|
||||
- Bookmarking (Joel Klabo)
|
||||
|
||||
### Changed
|
||||
|
||||
- No more inline npubs when tagging users (Swift)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix alignment of side menu labels (Joel Klabo)
|
||||
- Fix duplicated participants in reply-to view (Joel Klabo)
|
||||
- Load missing profiles in Zaps view (William Casarin)
|
||||
- Fix memory leak with inline videos (William Casarin)
|
||||
- Eliminate popping when scrolling (William Casarin)
|
||||
|
||||
|
||||
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
|
||||
|
||||
## [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 +650,4 @@
|
||||
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
@@ -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... :)
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
@@ -42,6 +47,12 @@
|
||||
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 */; };
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
|
||||
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; };
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.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 */; };
|
||||
@@ -92,6 +103,9 @@
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
|
||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
|
||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
|
||||
@@ -121,6 +135,8 @@
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
|
||||
@@ -155,6 +171,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 +217,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,15 +225,19 @@
|
||||
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 */; };
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
|
||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
|
||||
@@ -249,6 +273,21 @@
|
||||
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>"; };
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; };
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
|
||||
3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; 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 +298,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 +357,12 @@
|
||||
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>"; };
|
||||
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = "<group>"; };
|
||||
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.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>"; };
|
||||
@@ -392,6 +443,9 @@
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
|
||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = "<group>"; };
|
||||
4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = "<group>"; };
|
||||
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = "<group>"; };
|
||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
|
||||
@@ -421,6 +475,8 @@
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
||||
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
|
||||
@@ -455,6 +511,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,21 +560,26 @@
|
||||
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>"; };
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.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>"; };
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
|
||||
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
|
||||
@@ -624,6 +688,7 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||
@@ -644,6 +709,7 @@
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
|
||||
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||
@@ -655,7 +721,6 @@
|
||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
|
||||
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
|
||||
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
||||
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||
@@ -663,13 +728,36 @@
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
|
||||
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */,
|
||||
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */,
|
||||
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */,
|
||||
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C54AA0829A55416003E4487 /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C54AA0929A55429003E4487 /* EventGroup.swift */,
|
||||
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */,
|
||||
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
|
||||
4CE879562996C44A00F758CC /* Zaps */,
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */,
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */,
|
||||
@@ -681,6 +769,7 @@
|
||||
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||
4C363A8728236948006E126D /* BlocksView.swift */,
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
|
||||
4C285C8128385570008A31F1 /* CarouselView.swift */,
|
||||
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */,
|
||||
@@ -700,6 +789,7 @@
|
||||
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
|
||||
4C75EFAC28049CFB0006080F /* PostButton.swift */,
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */,
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
|
||||
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
@@ -780,6 +870,11 @@
|
||||
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
||||
3AB72AB8298ECF30004BB58C /* Translator.swift */,
|
||||
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -821,6 +916,7 @@
|
||||
children = (
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@@ -842,6 +938,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 +965,7 @@
|
||||
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */,
|
||||
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -919,6 +1025,7 @@
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
|
||||
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
|
||||
@@ -930,6 +1037,10 @@
|
||||
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
|
||||
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -964,6 +1075,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
@@ -1116,6 +1228,11 @@
|
||||
"zh-CN",
|
||||
"el-GR",
|
||||
ja,
|
||||
id,
|
||||
cs,
|
||||
ru,
|
||||
"zh-HK",
|
||||
"zh-TW",
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
@@ -1172,6 +1289,7 @@
|
||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
|
||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
|
||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
@@ -1184,6 +1302,7 @@
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
@@ -1195,8 +1314,10 @@
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||
@@ -1210,6 +1331,8 @@
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.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 */,
|
||||
@@ -1223,6 +1346,7 @@
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||
@@ -1234,6 +1358,7 @@
|
||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
|
||||
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
|
||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||
@@ -1242,11 +1367,12 @@
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
|
||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||
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 +1380,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 */,
|
||||
@@ -1270,15 +1397,19 @@
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
|
||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
|
||||
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
|
||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
|
||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||
4C54AA0A29A55429003E4487 /* EventGroup.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 */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
@@ -1299,6 +1430,7 @@
|
||||
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
||||
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
||||
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
|
||||
@@ -1327,6 +1459,8 @@
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
|
||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
|
||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
@@ -1342,6 +1476,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 */,
|
||||
@@ -1350,6 +1485,7 @@
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
@@ -1365,8 +1501,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
@@ -1374,7 +1513,9 @@
|
||||
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 */,
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1421,6 +1562,11 @@
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */,
|
||||
3A66D929299472FA008B44F4 /* ja */,
|
||||
3A41E55B299D52BE001FA465 /* id */,
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */,
|
||||
3A827A1A299FC69D00C4D171 /* ru */,
|
||||
3A3040FB29A91F03008A0F29 /* zh-HK */,
|
||||
3A3040FD29A91F31008A0F29 /* zh-TW */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1441,6 +1587,11 @@
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */,
|
||||
3A66D927299472FA008B44F4 /* ja */,
|
||||
3A41E559299D52BE001FA465 /* id */,
|
||||
3A8624D9299E82BE00BD8BE9 /* cs */,
|
||||
3A827A18299FC69D00C4D171 /* ru */,
|
||||
3A3040F929A91ED6008A0F29 /* zh-HK */,
|
||||
3A3040FC29A91F31008A0F29 /* zh-TW */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1461,6 +1612,12 @@
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */,
|
||||
3A66D928299472FA008B44F4 /* ja */,
|
||||
3A41E55A299D52BE001FA465 /* id */,
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */,
|
||||
3A827A19299FC69D00C4D171 /* ru */,
|
||||
3A3040FA29A91EFC008A0F29 /* zh-HK */,
|
||||
3A3040FE29A91F31008A0F29 /* zh-TW */,
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1596,7 +1753,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1638,7 +1795,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 321 B |
|
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
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
||||
|
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" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bbw.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bitcoin-p2p.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "blixt-wallet.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bluewallet.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "breez.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "cashapp.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 458 B |
@@ -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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 671 B |
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "lnlink.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "damus-nobg.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "muun.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "phoenix.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "river.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "strike.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "zebedee.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "zeus.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -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,16 +199,13 @@ 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
|
||||
}
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.cornerRadius(10)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
@@ -251,11 +218,11 @@ struct ImageCarousel: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.clipped()
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ struct UserView: View {
|
||||
let pubkey: String
|
||||
|
||||
var body: some View {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||
let followers = FollowersModel(damus_state: damus_state, target: pubkey)
|
||||
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum ZappingEventType {
|
||||
case failed(ZappingError)
|
||||
case got_zap_invoice(String)
|
||||
}
|
||||
|
||||
enum ZappingError {
|
||||
case fetching_invoice
|
||||
case bad_lnurl
|
||||
}
|
||||
|
||||
struct ZappingEvent {
|
||||
let is_custom: Bool
|
||||
let type: ZappingEventType
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
struct ZapButton: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
@@ -19,61 +35,8 @@ struct ZapButton: View {
|
||||
@State var slider_value: Double = 0.0
|
||||
@State var slider_visible: Bool = false
|
||||
@State var showing_select_wallet: Bool = false
|
||||
|
||||
func send_zap() {
|
||||
guard let privkey = damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
// TODO: gather comment?
|
||||
let content = ""
|
||||
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
|
||||
|
||||
zapping = true
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//damus_state.pool.send(.event(zapreq))
|
||||
}
|
||||
@State var showing_zap_customizer: Bool = false
|
||||
@State var is_charging: Bool = false
|
||||
|
||||
var zap_img: String {
|
||||
if bar.zapped {
|
||||
@@ -92,6 +55,10 @@ struct ZapButton: View {
|
||||
return Color.orange
|
||||
}
|
||||
|
||||
if is_charging {
|
||||
return Color.yellow
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return nil
|
||||
}
|
||||
@@ -100,24 +67,63 @@ struct ZapButton: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
EventActionButton(img: zap_img, col: zap_color) {
|
||||
if bar.zapped {
|
||||
//notify(.delete, bar.our_tip)
|
||||
} else if !zapping {
|
||||
send_zap()
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: zap_img)
|
||||
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
|
||||
.font(.footnote.weight(.medium))
|
||||
.onTapGesture {
|
||||
if bar.zapped {
|
||||
//notify(.delete, bar.our_tip)
|
||||
} else if !zapping {
|
||||
self.showing_zap_customizer = true
|
||||
//send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false)
|
||||
//self.zapping = true
|
||||
}
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0, pressing: { is_charing in
|
||||
self.is_charging = is_charging
|
||||
}, perform: {
|
||||
self.showing_zap_customizer = true
|
||||
})
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
|
||||
if bar.zap_total > 0 {
|
||||
Text(verbatim: format_msats_abbrev(bar.zap_total))
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
|
||||
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
|
||||
.offset(x: 22)
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||
}
|
||||
.sheet(isPresented: $showing_zap_customizer) {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard zap_ev.event.id == self.event.id else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
|
||||
self.zapping = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,3 +135,56 @@ struct ZapButton_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
guard let keypair = damus_state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
let content = comment ?? ""
|
||||
|
||||
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||
DispatchQueue.main.async {
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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,10 +163,10 @@ 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.")
|
||||
Text(verbatim: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
}
|
||||
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
|
||||
@@ -615,7 +615,8 @@ struct ContentView: View {
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts()
|
||||
drafts: Drafts(),
|
||||
events: EventCache()
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// BookmarksManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Joel Klabo on 2/18/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BookmarksManager {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let pubkey: String
|
||||
|
||||
init(pubkey: String) {
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
var bookmarks: [String] {
|
||||
get {
|
||||
return userDefaults.stringArray(forKey: storageKey()) ?? []
|
||||
}
|
||||
set {
|
||||
let uniqueBookmarks = Array(Set(newValue))
|
||||
if uniqueBookmarks != bookmarks {
|
||||
userDefaults.set(uniqueBookmarks, forKey: storageKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isBookmarked(_ string: String) -> Bool {
|
||||
return bookmarks.contains(string)
|
||||
}
|
||||
|
||||
func updateBookmark(_ string: String) {
|
||||
if isBookmarked(string) {
|
||||
bookmarks = bookmarks.filter { $0 != string }
|
||||
} else {
|
||||
bookmarks.append(string)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAll() {
|
||||
bookmarks = []
|
||||
}
|
||||
|
||||
private func storageKey() -> String {
|
||||
pk_setting_key(pubkey, key: "bookmarks")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ struct DamusState {
|
||||
let relay_filters: RelayFilters
|
||||
let relay_metadata: RelayMetadatas
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
@@ -32,9 +33,8 @@ struct DamusState {
|
||||
var is_privkey_user: Bool {
|
||||
keypair.privkey != nil
|
||||
}
|
||||
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts())
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
import Foundation
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: String = ""
|
||||
@Published var replies: [NostrEvent: String] = [:]
|
||||
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||
}
|
||||
|
||||
@@ -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, load: .from_events(events), damus_state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,33 @@ class HomeModel: ObservableObject {
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications: [NostrEvent] = []
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var dms: DirectMessagesModel
|
||||
@Published var events: [NostrEvent] = []
|
||||
@Published var events = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var signal: SignalModel = SignalModel()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||
}
|
||||
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
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) {
|
||||
@@ -114,21 +125,23 @@ class HomeModel: ObservableObject {
|
||||
handle_channel_meta(ev)
|
||||
case .zap:
|
||||
handle_zap_event(ev)
|
||||
case .zap_request:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.zaps.add_zap(zap: zap)
|
||||
|
||||
guard zap.target.pubkey == our_pubkey else {
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||
if !notifications.insert_zap(zap) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,8 +155,9 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
let our_keypair = damus_state.keypair
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
|
||||
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,7 +176,7 @@ class HomeModel: ObservableObject {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
|
||||
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,9 +194,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) {
|
||||
@@ -214,7 +228,7 @@ class HomeModel: ObservableObject {
|
||||
guard inner_ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if inner_ev.is_textlike {
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
}
|
||||
@@ -240,12 +254,11 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
// CHECK SIGS ON THESE
|
||||
|
||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
handle_notification(ev: ev)
|
||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||
notify(.liked, liked)
|
||||
notify(.update_stats, e.ref_id)
|
||||
@@ -303,10 +316,11 @@ class HomeModel: ObservableObject {
|
||||
case .eose(let sub_id):
|
||||
|
||||
if sub_id == dms_subid {
|
||||
let dms = dms.dms.flatMap { $0.1.events }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
|
||||
var dms = dms.dms.flatMap { $0.1.events }
|
||||
dms.append(contentsOf: incoming_dms)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_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, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
@@ -359,7 +373,6 @@ class HomeModel: ObservableObject {
|
||||
// TODO: separate likes?
|
||||
var home_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
@@ -369,7 +382,6 @@ class HomeModel: ObservableObject {
|
||||
|
||||
var notifications_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
@@ -445,10 +457,19 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
if let inner_ev = ev.inner_event {
|
||||
damus_state.events.insert(inner_ev)
|
||||
}
|
||||
|
||||
if !notifications.insert_event(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
handle_last_event(ev: ev, timeline: .notifications)
|
||||
}
|
||||
|
||||
@@ -458,29 +479,47 @@ 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) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
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 = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,14 +665,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))
|
||||
}
|
||||
@@ -761,14 +800,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
|
||||
|
||||
@@ -795,15 +831,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -237,17 +263,19 @@ func format_msats_abbrev(_ msats: Int64) -> String {
|
||||
return formatter.string(from: sats) ?? sats.stringValue
|
||||
}
|
||||
|
||||
func format_msats(_ msat: Int64) -> String {
|
||||
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.roundingMode = .down
|
||||
numberFormatter.locale = locale
|
||||
|
||||
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)
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
return String(format: bundle.localizedString(forKey: "sats_count", value: nil, table: nil), locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
}
|
||||
|
||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// ReactionGroup.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class EventGroup {
|
||||
var events: [NostrEvent]
|
||||
|
||||
var last_event_at: Int64 {
|
||||
guard let first = self.events.first else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return first.created_at
|
||||
}
|
||||
|
||||
init() {
|
||||
self.events = []
|
||||
}
|
||||
|
||||
init(events: [NostrEvent]) {
|
||||
self.events = events
|
||||
}
|
||||
|
||||
func insert(_ ev: NostrEvent) -> Bool {
|
||||
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ZapGroup.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapGroup {
|
||||
var zaps: [Zap]
|
||||
var msat_total: Int64
|
||||
var zappers: Set<String>
|
||||
|
||||
var last_event_at: Int64 {
|
||||
guard let first = zaps.first else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return first.event.created_at
|
||||
}
|
||||
|
||||
func zap_requests() -> [NostrEvent] {
|
||||
zaps.map { z in
|
||||
if let priv = z.private_request {
|
||||
return priv
|
||||
} else {
|
||||
return z.request.ev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(zaps: [Zap]) {
|
||||
self.zaps = zaps
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
init() {
|
||||
self.zaps = []
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
func insert(_ zap: Zap) -> Bool {
|
||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||
return false
|
||||
}
|
||||
|
||||
msat_total += zap.invoice.amount
|
||||
|
||||
if !zappers.contains(zap.request.ev.pubkey) {
|
||||
zappers.insert(zap.request.ev.pubkey)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
//
|
||||
// NotificationsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NotificationItem {
|
||||
case repost(String, EventGroup)
|
||||
case reaction(String, EventGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
case event_zap(String, ZapGroup)
|
||||
case reply(NostrEvent)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .repost(let evid, _):
|
||||
return "repost_" + evid
|
||||
case .reaction(let evid, _):
|
||||
return "reaction_" + evid
|
||||
case .profile_zap:
|
||||
return "profile_zap"
|
||||
case .event_zap(let evid, _):
|
||||
return "event_zap_" + evid
|
||||
case .reply(let ev):
|
||||
return "reply_" + ev.id
|
||||
}
|
||||
}
|
||||
|
||||
var last_event_at: Int64 {
|
||||
switch self {
|
||||
case .reaction(_, let evgrp):
|
||||
return evgrp.last_event_at
|
||||
case .repost(_, let evgrp):
|
||||
return evgrp.last_event_at
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp.last_event_at
|
||||
case .event_zap(_, let zapgrp):
|
||||
return zapgrp.last_event_at
|
||||
case .reply(let reply):
|
||||
return reply.created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
var incoming_zaps: [Zap]
|
||||
var incoming_events: [NostrEvent]
|
||||
var should_queue: Bool
|
||||
|
||||
// mappings from events to
|
||||
var zaps: [String: ZapGroup]
|
||||
var profile_zaps: ZapGroup
|
||||
var reactions: [String: EventGroup]
|
||||
var reposts: [String: EventGroup]
|
||||
var replies: [NostrEvent]
|
||||
var has_reply: Set<String>
|
||||
|
||||
@Published var notifications: [NotificationItem]
|
||||
|
||||
init() {
|
||||
self.zaps = [:]
|
||||
self.reactions = [:]
|
||||
self.reposts = [:]
|
||||
self.replies = []
|
||||
self.has_reply = Set()
|
||||
self.should_queue = true
|
||||
self.incoming_zaps = []
|
||||
self.incoming_events = []
|
||||
self.profile_zaps = ZapGroup()
|
||||
self.notifications = []
|
||||
}
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
self.should_queue = val
|
||||
}
|
||||
|
||||
func uniq_pubkeys() -> [String] {
|
||||
var pks = Set<String>()
|
||||
|
||||
for ev in incoming_events {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
for grp in reposts {
|
||||
for ev in grp.value.events {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
for ev in replies {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
for zap in incoming_zaps {
|
||||
pks.insert(zap.request.ev.pubkey)
|
||||
}
|
||||
|
||||
return Array(pks)
|
||||
}
|
||||
|
||||
func build_notifications() -> [NotificationItem] {
|
||||
var notifs: [NotificationItem] = []
|
||||
|
||||
for el in zaps {
|
||||
let evid = el.key
|
||||
let zapgrp = el.value
|
||||
|
||||
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
||||
notifs.append(notif)
|
||||
}
|
||||
|
||||
if !profile_zaps.zaps.isEmpty {
|
||||
notifs.append(.profile_zap(profile_zaps))
|
||||
}
|
||||
|
||||
for el in reposts {
|
||||
let evid = el.key
|
||||
let evgrp = el.value
|
||||
|
||||
notifs.append(.repost(evid, evgrp))
|
||||
}
|
||||
|
||||
for el in reactions {
|
||||
let evid = el.key
|
||||
let evgrp = el.value
|
||||
|
||||
notifs.append(.reaction(evid, evgrp))
|
||||
}
|
||||
|
||||
for reply in replies {
|
||||
notifs.append(.reply(reply))
|
||||
}
|
||||
|
||||
notifs.sort { $0.last_event_at > $1.last_event_at }
|
||||
return notifs
|
||||
}
|
||||
|
||||
|
||||
private func insert_repost(_ ev: NostrEvent) -> Bool {
|
||||
guard let reposted_ev = ev.inner_event else {
|
||||
return false
|
||||
}
|
||||
|
||||
let id = reposted_ev.id
|
||||
|
||||
if let evgrp = self.reposts[id] {
|
||||
return evgrp.insert(ev)
|
||||
} else {
|
||||
let evgrp = EventGroup()
|
||||
self.reposts[id] = evgrp
|
||||
return evgrp.insert(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func insert_text(_ ev: NostrEvent) -> Bool {
|
||||
guard !has_reply.contains(ev.id) else {
|
||||
return false
|
||||
}
|
||||
|
||||
has_reply.insert(ev.id)
|
||||
replies.append(ev)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func insert_reaction(_ ev: NostrEvent) -> Bool {
|
||||
guard let ref_id = ev.referenced_ids.last else {
|
||||
return false
|
||||
}
|
||||
|
||||
let id = ref_id.id
|
||||
|
||||
if let evgrp = self.reactions[id] {
|
||||
return evgrp.insert(ev)
|
||||
} else {
|
||||
let evgrp = EventGroup()
|
||||
self.reactions[id] = evgrp
|
||||
return evgrp.insert(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
|
||||
if ev.known_kind == .boost {
|
||||
return insert_repost(ev)
|
||||
} else if ev.known_kind == .like {
|
||||
return insert_reaction(ev)
|
||||
} else if ev.known_kind == .text {
|
||||
return insert_text(ev)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func insert_zap_immediate(_ zap: Zap) -> Bool {
|
||||
switch zap.target {
|
||||
case .note(let notezt):
|
||||
let id = notezt.note_id
|
||||
if let zapgrp = self.zaps[notezt.note_id] {
|
||||
return zapgrp.insert(zap)
|
||||
} else {
|
||||
let zapgrp = ZapGroup()
|
||||
self.zaps[id] = zapgrp
|
||||
return zapgrp.insert(zap)
|
||||
}
|
||||
|
||||
case .profile:
|
||||
return profile_zaps.insert(zap)
|
||||
}
|
||||
}
|
||||
|
||||
func insert_event(_ ev: NostrEvent) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||
}
|
||||
|
||||
if insert_event_immediate(ev) {
|
||||
self.notifications = build_notifications()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func insert_zap(_ zap: Zap) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||
}
|
||||
|
||||
if insert_zap_immediate(zap) {
|
||||
self.notifications = build_notifications()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||
var changed = false
|
||||
var count = 0
|
||||
|
||||
count = incoming_events.count
|
||||
incoming_events = incoming_events.filter(isIncluded)
|
||||
changed = changed || incoming_events.count != count
|
||||
|
||||
count = profile_zaps.zaps.count
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||
changed = changed || profile_zaps.zaps.count != count
|
||||
|
||||
for el in reactions {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in reposts {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in zaps {
|
||||
count = el.value.zaps.count
|
||||
el.value.zaps = el.value.zaps.filter {
|
||||
isIncluded($0.request.ev)
|
||||
}
|
||||
changed = changed || el.value.zaps.count != count
|
||||
}
|
||||
|
||||
count = replies.count
|
||||
replies = replies.filter(isIncluded)
|
||||
changed = changed || replies.count != count
|
||||
|
||||
if changed {
|
||||
self.notifications = build_notifications()
|
||||
}
|
||||
}
|
||||
|
||||
func flush() -> Bool {
|
||||
var inserted = false
|
||||
|
||||
for zap in incoming_zaps {
|
||||
inserted = insert_zap_immediate(zap) || inserted
|
||||
}
|
||||
|
||||
for event in incoming_events {
|
||||
inserted = insert_event_immediate(event) || inserted
|
||||
}
|
||||
|
||||
if inserted {
|
||||
self.notifications = build_notifications()
|
||||
}
|
||||
|
||||
return inserted
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, load: .from_events(events.all_events), damus_state: damus_state)
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
|
||||
case .from_keys(let pks):
|
||||
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] {
|
||||
for pk in pks {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for ev in events {
|
||||
@@ -112,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) {
|
||||
enum PubkeysToLoad {
|
||||
case from_events([NostrEvent])
|
||||
case from_keys([String])
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events)
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
|
||||
filter.authors = authors
|
||||
|
||||
guard !authors.isEmpty else {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
if sub_id == self.base_subid {
|
||||
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
|
||||
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class ZapsModel: ObservableObject {
|
||||
var zaps: [Zap]
|
||||
|
||||
let zaps_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
init(state: DamusState, target: ZapTarget) {
|
||||
self.state = state
|
||||
@@ -44,34 +45,39 @@ class ZapsModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard case .event(_, let ev) = resp else {
|
||||
return
|
||||
}
|
||||
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
switch resp {
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
let events = self.zaps.map { $0.request.ev }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
case .event(_, let ev):
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ struct Profile: Codable {
|
||||
}
|
||||
|
||||
static func displayName(profile: Profile?, pubkey: String) -> String {
|
||||
if pubkey == "anon" {
|
||||
return "Anonymous"
|
||||
}
|
||||
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
|
||||
return profile?.name ?? abbrev_pubkey(pk)
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
pubkey = refkey.ref_id
|
||||
}
|
||||
|
||||
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content)
|
||||
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
|
||||
self.decrypted_content = dec
|
||||
|
||||
return dec
|
||||
@@ -168,6 +168,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
/*
|
||||
switch validity {
|
||||
case .ok:
|
||||
return content
|
||||
@@ -176,6 +179,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
case .bad_sig:
|
||||
return content + "\n\n*WARNING: invalid signature, could be forged!*"
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
var description: String {
|
||||
@@ -278,21 +282,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 +290,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) {
|
||||
@@ -587,14 +577,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
||||
}
|
||||
}
|
||||
|
||||
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
|
||||
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
|
||||
// target tags must be the same as zap request target tags
|
||||
let tags = zap_target_to_tags(target)
|
||||
|
||||
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
|
||||
note.id = calculate_event_id(ev: note)
|
||||
note.sig = sign_event(privkey: identity.privkey, ev: note)
|
||||
|
||||
guard let note_json = encode_json(note) else {
|
||||
return nil
|
||||
}
|
||||
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
}
|
||||
|
||||
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
|
||||
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let enc_note = anon_tag[1]
|
||||
|
||||
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
|
||||
|
||||
// check to see if the private note was from us
|
||||
if note == nil {
|
||||
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
|
||||
return nil
|
||||
}
|
||||
// use our private keypair and their pubkey to get the shared secret
|
||||
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
|
||||
}
|
||||
|
||||
guard let note else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard note.kind == 9733 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zr_etag = zapreq.referenced_ids.first
|
||||
let note_etag = note.referenced_ids.first
|
||||
|
||||
guard zr_etag == note_etag else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zr_ptag = zapreq.referenced_pubkeys.first
|
||||
let note_ptag = note.referenced_pubkeys.first
|
||||
|
||||
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard validate_event(ev: note) == .ok else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
|
||||
let to_hash = our_privkey + id + String(created_at)
|
||||
guard let dat = to_hash.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let privkey_bytes = sha256(dat)
|
||||
let privkey = hex_encode(privkey_bytes)
|
||||
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return FullKeypair(pubkey: pubkey, privkey: privkey)
|
||||
}
|
||||
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
tags.append(relay_tag)
|
||||
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
|
||||
|
||||
var kp = keypair
|
||||
|
||||
let now = Int64(Date().timeIntervalSince1970)
|
||||
|
||||
var message = content
|
||||
switch zap_type {
|
||||
case .pub:
|
||||
break
|
||||
case .non_zap:
|
||||
break
|
||||
case .anon:
|
||||
tags.append(["anon"])
|
||||
kp = generate_new_keypair().to_full()!
|
||||
case .priv:
|
||||
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
|
||||
return nil
|
||||
}
|
||||
kp = priv_kp
|
||||
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
|
||||
return nil
|
||||
}
|
||||
tags.append(["anon", privreq])
|
||||
message = ""
|
||||
}
|
||||
|
||||
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
|
||||
ev.id = calculate_event_id(ev: ev)
|
||||
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
@@ -624,14 +715,14 @@ func event_to_json(ev: NostrEvent) -> String {
|
||||
return str
|
||||
}
|
||||
|
||||
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? {
|
||||
func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
|
||||
guard let privkey = privkey else {
|
||||
return nil
|
||||
}
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
|
||||
return nil
|
||||
}
|
||||
guard let dat = decode_dm_base64(content) else {
|
||||
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
|
||||
return nil
|
||||
}
|
||||
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
|
||||
@@ -640,6 +731,13 @@ func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String?
|
||||
return String(data: dat, encoding: .utf8)
|
||||
}
|
||||
|
||||
func decrypt_note(our_privkey: String, their_pubkey: String, enc_note: String, encoding: EncEncoding) -> NostrEvent? {
|
||||
guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return decode_nostr_event_json(json: dec)
|
||||
}
|
||||
|
||||
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
|
||||
guard let privkey_bytes = try? privkey.bytes else {
|
||||
@@ -685,6 +783,39 @@ struct DirectMessageBase64 {
|
||||
let iv: [UInt8]
|
||||
}
|
||||
|
||||
|
||||
|
||||
func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String {
|
||||
let content_bech32 = bech32_encode(hrp: "pzap", content)
|
||||
let iv_bech32 = bech32_encode(hrp: "iv", iv)
|
||||
return content_bech32 + "_" + iv_bech32
|
||||
}
|
||||
|
||||
func decode_dm_bech32(_ all: String) -> DirectMessageBase64? {
|
||||
let parts = all.split(separator: "_")
|
||||
guard parts.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let content_bech32 = String(parts[0])
|
||||
let iv_bech32 = String(parts[1])
|
||||
|
||||
guard let content_tup = try? bech32_decode(content_bech32) else {
|
||||
return nil
|
||||
}
|
||||
guard let iv_tup = try? bech32_decode(iv_bech32) else {
|
||||
return nil
|
||||
}
|
||||
guard content_tup.hrp == "pzap" else {
|
||||
return nil
|
||||
}
|
||||
guard iv_tup.hrp == "iv" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes)
|
||||
}
|
||||
|
||||
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
|
||||
let content_b64 = base64_encode(content)
|
||||
let iv_b64 = base64_encode(iv)
|
||||
@@ -849,7 +980,7 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
||||
extension [ReferencedId] {
|
||||
var pRefs: [ReferencedId] {
|
||||
get {
|
||||
self.filter { ref in
|
||||
Set(self).filter { ref in
|
||||
ref.key == "p"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ enum NostrKind: Int {
|
||||
case chat = 42
|
||||
case list = 30000
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func char_to_hex(_ c: UInt8) -> UInt8?
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
func hex_decode(_ str: String) -> [UInt8]?
|
||||
{
|
||||
if str.count == 0 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// EventCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class EventCache {
|
||||
private var events: [String: NostrEvent]
|
||||
|
||||
func lookup(_ evid: String) -> NostrEvent? {
|
||||
return events[evid]
|
||||
}
|
||||
|
||||
func insert(_ ev: NostrEvent) {
|
||||
guard events[ev.id] == nil else {
|
||||
return
|
||||
}
|
||||
events[ev.id] = ev
|
||||
}
|
||||
|
||||
init() {
|
||||
self.events = [:]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// 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, ScrollQueue {
|
||||
private var has_event: Set<String>
|
||||
@Published var events: [NostrEvent]
|
||||
@Published var incoming: [NostrEvent]
|
||||
var should_queue: Bool
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
self.should_queue = val
|
||||
}
|
||||
|
||||
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 = []
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
|
||||
return true
|
||||
}
|
||||
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
|
||||
var i: Int = 0
|
||||
|
||||
for zap in zaps {
|
||||
@@ -47,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if new_zap.invoice.amount > zap.invoice.amount {
|
||||
if cmp(new_zap, zap) {
|
||||
zaps.insert(new_zap, at: i)
|
||||
return true
|
||||
}
|
||||
@@ -58,6 +58,27 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.event.created_at > b.event.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.invoice.amount > b.invoice.amount
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import Foundation
|
||||
|
||||
struct LNUrlPayRequest: Decodable {
|
||||
let allowsNostr: Bool?
|
||||
let commentAllowed: Int?
|
||||
let nostrPubkey: String?
|
||||
|
||||
let metadata: String?
|
||||
let minSendable: Int64?
|
||||
let maxSendable: Int64?
|
||||
let status: String?
|
||||
|
||||
@@ -10,10 +10,11 @@ import LinkPresentation
|
||||
|
||||
class CustomLinkView: LPLinkView {
|
||||
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
|
||||
|
||||
}
|
||||
|
||||
enum Metadata {
|
||||
case linkmeta(LPLinkMetadata)
|
||||
case linkmeta(CachedMetadata)
|
||||
case url(URL)
|
||||
}
|
||||
|
||||
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> CustomLinkView {
|
||||
switch meta {
|
||||
case .linkmeta(let linkmeta):
|
||||
return CustomLinkView(metadata: linkmeta)
|
||||
return CustomLinkView(metadata: linkmeta.meta)
|
||||
case .url(let url):
|
||||
return CustomLinkView(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: CustomLinkView, context: Context) {
|
||||
switch meta {
|
||||
case .linkmeta(let cached):
|
||||
cached.intrinsic_height = uiView.intrinsicContentSize.height
|
||||
case .url:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// LocalizationUtil.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/24/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func bundleForLocale(locale: Locale?) -> Bundle {
|
||||
if locale == nil {
|
||||
return Bundle.main
|
||||
}
|
||||
|
||||
let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj")
|
||||
return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -101,6 +101,12 @@ extension Notification.Name {
|
||||
static var update_stats: Notification.Name {
|
||||
return Notification.Name("update_stats")
|
||||
}
|
||||
static var update_bookmarks: Notification.Name {
|
||||
return Notification.Name("update_bookmarks")
|
||||
}
|
||||
static var zapping: Notification.Name {
|
||||
return Notification.Name("zapping")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
@@ -8,8 +8,18 @@
|
||||
import Foundation
|
||||
import LinkPresentation
|
||||
|
||||
class CachedMetadata {
|
||||
let meta: LPLinkMetadata
|
||||
var intrinsic_height: CGFloat?
|
||||
|
||||
init(meta: LPLinkMetadata) {
|
||||
self.meta = meta
|
||||
self.intrinsic_height = nil
|
||||
}
|
||||
}
|
||||
|
||||
enum Preview {
|
||||
case value(LinkViewRepresentable)
|
||||
case value(CachedMetadata)
|
||||
case failed
|
||||
}
|
||||
|
||||
@@ -20,12 +30,12 @@ class PreviewCache {
|
||||
return previews[evid]
|
||||
}
|
||||
|
||||
func store(evid: String, preview: LinkViewRepresentable?) {
|
||||
func store(evid: String, preview: LPLinkMetadata?) {
|
||||
switch preview {
|
||||
case .none:
|
||||
previews[evid] = .failed
|
||||
case .some(let meta):
|
||||
previews[evid] = .value(meta)
|
||||
previews[evid] = .value(CachedMetadata(meta: meta))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current
|
||||
return formatter.string(from: DateComponents(calendar: calendar, second: second))!
|
||||
}
|
||||
|
||||
return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred")
|
||||
let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current)
|
||||
return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred")
|
||||
}
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ZapSource {
|
||||
case author(String)
|
||||
// TODO: anonymous
|
||||
//case anonymous
|
||||
}
|
||||
|
||||
public struct NoteZapTarget: Equatable {
|
||||
public let note_id: String
|
||||
public let author: String
|
||||
@@ -55,8 +49,10 @@ struct Zap {
|
||||
public let zapper: String /// zap authorizer
|
||||
public let target: ZapTarget
|
||||
public let request: ZapRequest
|
||||
public let is_anon: Bool
|
||||
public let private_request: NostrEvent?
|
||||
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
|
||||
/// Make sure that we only create a zap event if it is authorized by the profile or event
|
||||
guard zapper == zap_ev.pubkey else {
|
||||
return nil
|
||||
@@ -83,14 +79,26 @@ struct Zap {
|
||||
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let zap_req = decode_nostr_event_json(desc) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard validate_event(ev: zap_req) == .ok else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let target = determine_zap_target(zap_req) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
|
||||
let private_request = our_privkey.flatMap {
|
||||
decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
|
||||
}
|
||||
|
||||
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
|
||||
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +293,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) async -> String? {
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
@@ -295,11 +303,16 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int)
|
||||
|
||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||
|
||||
if zappable {
|
||||
if let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
query.append(URLQueryItem(name: "nostr", value: json))
|
||||
}
|
||||
if let zapreq, zappable && zap_type != .non_zap {
|
||||
let json = event_to_json(ev: zapreq)
|
||||
print("zapreq json: \(json)")
|
||||
query.append(URLQueryItem(name: "nostr", value: json))
|
||||
}
|
||||
|
||||
// add a lud12 comment as well if we have it
|
||||
if let comment, let limit = payreq.commentAllowed, limit != 0 {
|
||||
let limited_comment = String(comment.prefix(limit))
|
||||
query.append(URLQueryItem(name: "comment", value: limited_comment))
|
||||
}
|
||||
|
||||
base_url.queryItems = query
|
||||
|
||||
@@ -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_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
|
||||
}
|
||||
case .profile(_):
|
||||
break
|
||||
@@ -61,7 +61,5 @@ class Zaps {
|
||||
event_totals[id] = event_totals[id]! + zap.invoice.amount
|
||||
|
||||
notify(.update_stats, zap.target.id)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,19 @@ struct EventActionBar: View {
|
||||
let event: NostrEvent
|
||||
let test_lnurl: String?
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
let thread: ThreadV2?
|
||||
|
||||
// just used for previews
|
||||
@State var sheet: ActionBarSheet? = nil
|
||||
@State var confirm_boost: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil, thread: ThreadV2? = nil) {
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.test_lnurl = test_lnurl
|
||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||
self.thread = thread
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
@@ -47,21 +45,13 @@ struct EventActionBar: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
let self_replied = (thread != nil && thread!.childEvents.first { $0.pubkey == damus_state.pubkey } != nil)
|
||||
|
||||
EventActionButton(img: "bubble.left", col: self_replied ? Color.blue : nil) {
|
||||
EventActionButton(img: "bubble.left", col: nil) {
|
||||
notify(.reply, event)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
if thread != nil && !thread!.childEvents.isEmpty {
|
||||
Text("\(thread!.childEvents.count)")
|
||||
.offset(x: -10)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(self_replied ? Color.blue : Color.gray)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
ZStack {
|
||||
HStack(spacing: 4) {
|
||||
|
||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||
if bar.boosted {
|
||||
@@ -71,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)
|
||||
@@ -86,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)
|
||||
|
||||
@@ -168,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,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) {
|
||||
@@ -210,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)
|
||||
|
||||
@@ -26,14 +26,16 @@ 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'.")
|
||||
let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
|
||||
}
|
||||
.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'.")
|
||||
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
@@ -41,7 +43,8 @@ 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'.")
|
||||
let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
@@ -49,6 +52,21 @@ struct EventDetailBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
return String(format: bundle.localizedString(forKey: "reposts_count", value: nil, table: nil), locale: locale, count)
|
||||
}
|
||||
|
||||
func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
return String(format: bundle.localizedString(forKey: "reactions_count", value: nil, table: nil), locale: locale, count)
|
||||
}
|
||||
|
||||
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
return String(format: bundle.localizedString(forKey: "zaps_count", value: nil, table: nil), locale: locale, count)
|
||||
}
|
||||
|
||||
struct EventDetailBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// BookmarksView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Joel Klabo on 2/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
let state: DamusState
|
||||
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
|
||||
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
|
||||
|
||||
@State private var bookmarkEvents: [NostrEvent] = []
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if bookmarkEvents.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "bookmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
|
||||
}
|
||||
.task {
|
||||
updateBookmarks()
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
InnerTimelineView(events: EventHolder(events: bookmarkEvents, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(bookmarksTitle)
|
||||
.toolbar {
|
||||
if !bookmarkEvents.isEmpty {
|
||||
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
|
||||
BookmarksManager(pubkey: state.pubkey).clearAll()
|
||||
bookmarkEvents = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_bookmarks)) { _ in
|
||||
updateBookmarks()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBookmarks() {
|
||||
bookmarkEvents = BookmarksManager(pubkey: state.pubkey).bookmarks.compactMap { bookmark_json in
|
||||
event_from_json(dat: bookmark_json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct BookmarksView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BookmarksView()
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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,9 @@ struct ChatView: View {
|
||||
NoteContentView(damus_state: damus_state,
|
||||
event: event,
|
||||
show_images: show_images,
|
||||
size: .normal,
|
||||
artifacts: .just_content(event.content),
|
||||
size: .normal)
|
||||
truncate: false)
|
||||
|
||||
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
|
||||
|
||||
@@ -8,6 +8,7 @@ import AVFoundation
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
import Combine
|
||||
|
||||
struct ConfigView: View {
|
||||
let state: DamusState
|
||||
@@ -128,9 +129,16 @@ 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
|
||||
|
||||
if let parsed = handle_string_amount(new_value: newValue) {
|
||||
self.default_zap_amount = String(parsed)
|
||||
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||
@@ -201,19 +209,13 @@ 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.")
|
||||
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
|
||||
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
|
||||
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
@@ -333,3 +335,19 @@ struct ConfigView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handle_string_amount(new_value: String) -> Int? {
|
||||
let digits = Set("0123456789")
|
||||
let filtered = new_value.filter { digits.contains($0) }
|
||||
|
||||
if filtered == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let amt = Int(filtered) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return amt
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -37,9 +37,7 @@ struct DMChatView: View {
|
||||
|
||||
var Header: some View {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
|
||||
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
|
||||
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
return NavigationLink(destination: profile_page) {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
|
||||
@@ -183,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
enum EncEncoding {
|
||||
case base64
|
||||
case bech32
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
@@ -198,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let created = created_at ?? Int64(Date().timeIntervalSince1970)
|
||||
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)
|
||||
|
||||
|
||||