Compare commits
209 Commits
tyiu/relay
...
tyiu/trans
| 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 | ||
|
|
59211bb4fd | ||
|
|
6d634763c5 | ||
|
|
49cf56f4c2 | ||
|
|
98c7bf5afc | ||
| 70a7239cfd | |||
|
|
0e83632896 | ||
|
|
f0df4aa218 | ||
|
|
bb9fc6f905 | ||
| f69e0c660a | |||
|
|
9089246b6b | ||
|
|
237c939639 | ||
|
|
4f86361b63 | ||
|
|
209ad71ff3 | ||
|
|
c728850524 | ||
| bc638f79f6 | |||
|
|
47a6f7ff38 | ||
|
|
6653798d27 | ||
|
|
e9ea96ffb6 | ||
|
59ccde9c38
|
|||
|
f9be7b166c
|
|||
|
|
2366089896 | ||
|
|
4f2bacfaab | ||
|
|
5a8b29b5cc | ||
|
|
679c0ac424 | ||
|
|
48050f5e69 | ||
|
|
b5c967e161 | ||
|
|
715d4aa35d | ||
|
|
4b54278378 | ||
|
|
6e700e5726 | ||
|
|
18ad113198 | ||
|
|
7415671900 | ||
|
|
d5b6d935e8 | ||
| 543fd67f35 | |||
|
|
c24689d3ef | ||
| bf7120dc08 | |||
|
|
dbe938ad9b | ||
|
|
289d55b918 | ||
|
|
771fa845e3 | ||
|
|
5f1545b86a | ||
|
|
9a95967a81 | ||
|
|
fe444228e6 | ||
|
|
504108da75 | ||
|
|
d43a2ff92d | ||
|
|
10596ddb09 | ||
|
|
989684cd37 | ||
|
|
9ab03034a2 | ||
| c602c754f8 | |||
|
c5db887ab1
|
|||
|
|
0563ec8bf8 | ||
|
|
29c4170833 | ||
|
|
3c629621eb | ||
|
|
09f12845c0 | ||
|
|
b882a96206 | ||
|
|
fb82cc0531 | ||
|
|
90cd48ead7 | ||
|
|
f71b67f036 | ||
|
4406e44424
|
|||
|
|
ae6608cf7d | ||
|
|
04759107a2 | ||
|
|
552402f2b5 | ||
| 852609ee30 | |||
|
|
1e44d97a97 | ||
| 567303e680 | |||
| 7d1bac4028 | |||
|
|
eae844e081 | ||
|
|
140b0e4fc4 | ||
| 0b476faff7 | |||
|
|
53ec89551b | ||
|
|
638052492d | ||
|
|
45f8c37498 | ||
|
|
f96ad99790 | ||
|
|
1f79c20973 | ||
| e8b23daa3d | |||
|
|
4faf63f29d | ||
|
|
84ad0e03d0 |
24
.github/workflows/export-translations.yaml
vendored
Normal file
@@ -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
|
||||
137
CHANGELOG.md
@@ -1,3 +1,139 @@
|
||||
## [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
|
||||
|
||||
- Relay Filtering (William Casarin)
|
||||
- Japanese translations (Terry Yiu)
|
||||
- Add password autofill on account login and creation (Terry Yiu)
|
||||
- Show if relay is paid (William Casarin)
|
||||
- Add "Follows You" indicator on profile (William Casarin)
|
||||
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
|
||||
- Relay Detail View (Joel Klabo)
|
||||
- Warn when attempting to post an nsec key (Terry Yiu)
|
||||
- DeepL translation integration (Terry Yiu)
|
||||
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
|
||||
- Add accessibility labels to action bar (Bryan Montz)
|
||||
- Copy invoice button (Joel Klabo)
|
||||
- Receive Lightning Zaps (William Casarin)
|
||||
- Allow text selection in bio (Suhail Saqan)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Show "Follow Back" button on profile page (William Casarin)
|
||||
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
|
||||
- Updated QR code view, include profile image, etc (ericholguin)
|
||||
- Clicking relay numbers now goes to relay config (radixrat)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Load zaps, likes and reposts when you open a thread (William Casarin)
|
||||
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
|
||||
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
|
||||
- Fix sidebar navigation bugs (OlegAba)
|
||||
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
|
||||
- Make @ mentions case insensitive (William Casarin)
|
||||
- Fix some lnurls not getting decoded properly (William Casarin)
|
||||
- Hide incoming DMs from blocked users (William Casarin)
|
||||
- Hide blocked users from search results (William Casarin)
|
||||
- Fix Cash App invoice payments (Rob Seward)
|
||||
- DM Padding (OlegAba)
|
||||
- Check for broken lnurls (William Casarin)
|
||||
|
||||
|
||||
|
||||
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
|
||||
## [1.0.0-13] - 2023-01-30
|
||||
|
||||
### Added
|
||||
@@ -515,4 +651,3 @@
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
|
||||
18
README.md
@@ -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... :)
|
||||
|
||||
@@ -26,6 +26,10 @@ static inline int is_boundary(char c) {
|
||||
return !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
@@ -55,8 +59,8 @@ static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c) && consumedAtLeastOne)
|
||||
return 1;
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = true;
|
||||
@@ -221,6 +225,9 @@ static int parse_url(struct cursor *cur, struct block *block) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// strip any unwanted characters
|
||||
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
|
||||
|
||||
block->type = BLOCK_URL;
|
||||
block->block.str.start = (const char *)start;
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
@@ -11,10 +11,19 @@
|
||||
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 */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
||||
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||
@@ -28,7 +37,6 @@
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@@ -39,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 */; };
|
||||
@@ -89,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 */; };
|
||||
@@ -118,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 */; };
|
||||
@@ -139,6 +158,8 @@
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
|
||||
@@ -150,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 */; };
|
||||
@@ -163,6 +187,14 @@
|
||||
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
|
||||
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
|
||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; };
|
||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
|
||||
@@ -185,21 +217,29 @@
|
||||
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 */; };
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
|
||||
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 */; };
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -229,7 +269,25 @@
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -237,6 +295,15 @@
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -249,9 +316,13 @@
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
|
||||
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
|
||||
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
|
||||
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -276,7 +347,6 @@
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
@@ -287,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>"; };
|
||||
@@ -367,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>"; };
|
||||
@@ -396,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>"; };
|
||||
@@ -417,6 +498,8 @@
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
|
||||
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
|
||||
@@ -428,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>"; };
|
||||
@@ -443,6 +529,14 @@
|
||||
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
|
||||
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
|
||||
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
|
||||
4CE8795A2996C47A00F758CC /* ZapsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsModel.swift; sourceTree = "<group>"; };
|
||||
4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; };
|
||||
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; };
|
||||
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
|
||||
@@ -466,20 +560,28 @@
|
||||
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>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -586,6 +688,7 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||
@@ -606,6 +709,7 @@
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
|
||||
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||
@@ -617,17 +721,45 @@
|
||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
|
||||
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
|
||||
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
||||
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
|
||||
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 */,
|
||||
4CF0ABF42985CD4200D66079 /* Posting */,
|
||||
4CF0ABDF2981A83000D66079 /* Muting */,
|
||||
@@ -637,6 +769,7 @@
|
||||
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||
4C363A8728236948006E126D /* BlocksView.swift */,
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
|
||||
4C285C8128385570008A31F1 /* CarouselView.swift */,
|
||||
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */,
|
||||
@@ -656,6 +789,7 @@
|
||||
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
|
||||
4C75EFAC28049CFB0006080F /* PostButton.swift */,
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */,
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
|
||||
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
@@ -672,7 +806,6 @@
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
|
||||
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
|
||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||
@@ -683,6 +816,7 @@
|
||||
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -710,9 +844,11 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
|
||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||
4C363A8328233689006E126D /* Parser.swift */,
|
||||
@@ -733,6 +869,12 @@
|
||||
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
|
||||
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>";
|
||||
@@ -740,9 +882,14 @@
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879532996BA0000F758CC /* Detail */,
|
||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
|
||||
4C06670028FC7C5900038D2A /* RelayView.swift */,
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
@@ -764,6 +911,16 @@
|
||||
path = Reactions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -781,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 = (
|
||||
@@ -799,6 +965,7 @@
|
||||
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */,
|
||||
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -858,6 +1025,7 @@
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
|
||||
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
|
||||
@@ -869,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>";
|
||||
@@ -882,6 +1054,32 @@
|
||||
path = damusUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879492995B58700F758CC /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */,
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879532996BA0000F758CC /* Detail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
|
||||
);
|
||||
path = Detail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879562996C44A00F758CC /* Zaps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CEE2AE62804F57B00AB5EEF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1028,6 +1226,13 @@
|
||||
ar,
|
||||
nl,
|
||||
"zh-CN",
|
||||
"el-GR",
|
||||
ja,
|
||||
id,
|
||||
cs,
|
||||
ru,
|
||||
"zh-HK",
|
||||
"zh-TW",
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
@@ -1084,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 */,
|
||||
@@ -1093,8 +1299,10 @@
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||
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 */,
|
||||
@@ -1106,12 +1314,15 @@
|
||||
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 */,
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||
@@ -1120,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 */,
|
||||
@@ -1133,7 +1346,9 @@
|
||||
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 */,
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||
@@ -1143,17 +1358,21 @@
|
||||
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 */,
|
||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
|
||||
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 */,
|
||||
@@ -1161,28 +1380,36 @@
|
||||
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 */,
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
|
||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
||||
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
|
||||
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
|
||||
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
||||
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 */,
|
||||
@@ -1203,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 */,
|
||||
@@ -1224,36 +1452,48 @@
|
||||
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
|
||||
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
|
||||
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
|
||||
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 */,
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||
4C0A3F97280F8E02000448DE /* ThreadView.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 */,
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
|
||||
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 */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1261,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 */,
|
||||
@@ -1270,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;
|
||||
@@ -1315,6 +1560,13 @@
|
||||
3AC524F0298C000B00693EBF /* ar */,
|
||||
3A96D41C298DA94500388A2A /* nl */,
|
||||
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>";
|
||||
@@ -1333,6 +1585,13 @@
|
||||
3AC524EE298C000B00693EBF /* ar */,
|
||||
3A96D41A298DA94500388A2A /* nl */,
|
||||
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>";
|
||||
@@ -1351,6 +1610,14 @@
|
||||
3AC524EF298C000B00693EBF /* ar */,
|
||||
3A96D41B298DA94500388A2A /* nl */,
|
||||
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>";
|
||||
@@ -1486,7 +1753,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 13;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1494,6 +1761,7 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1508,7 +1776,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1527,7 +1795,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 13;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1535,6 +1803,7 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1549,7 +1818,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
|
||||
"version" : "7.4.0"
|
||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||
"version" : "7.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
11
damus/Assets.xcassets/bbw.imageset/Contents.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/ic-tick.imageset/ic-tick.png
vendored
|
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
|
||||
}
|
||||
|
||||
96
damus/Components/SelectableText.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// SelectableText.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Oleg Abalonski on 2/16/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct SelectableText: View {
|
||||
|
||||
let attributedString: AttributedString
|
||||
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
TextViewRepresentable(
|
||||
attributedString: attributedString,
|
||||
textColor: UIColor.label,
|
||||
font: UIFont.preferredFont(forTextStyle: .title2),
|
||||
fixedWidth: selectedTextWidth,
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.onAppear {
|
||||
self.selectedTextWidth = geo.size.width
|
||||
}
|
||||
.onChange(of: geo.size) { newSize in
|
||||
self.selectedTextWidth = newSize.width
|
||||
}
|
||||
}
|
||||
.frame(height: selectedTextHeight)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
|
||||
let attributedString: AttributedString
|
||||
let textColor: UIColor
|
||||
let font: UIFont
|
||||
let fixedWidth: CGFloat
|
||||
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||
let view = UITextView()
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
view.textContainer.lineFragmentPadding = 0
|
||||
view.textContainerInset = .zero
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||
let mutableAttributedString = createNSAttributedString()
|
||||
uiView.attributedText = mutableAttributedString
|
||||
|
||||
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
height = newHeight
|
||||
}
|
||||
}
|
||||
|
||||
func createNSAttributedString() -> NSMutableAttributedString {
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString)
|
||||
let myAttribute = [
|
||||
NSAttributedString.Key.font: font,
|
||||
NSAttributedString.Key.foregroundColor: textColor
|
||||
]
|
||||
|
||||
mutableAttributedString.addAttributes(
|
||||
myAttribute,
|
||||
range: NSRange.init(location: 0, length: mutableAttributedString.length)
|
||||
)
|
||||
|
||||
return mutableAttributedString
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension NSAttributedString {
|
||||
|
||||
func height(containerWidth: CGFloat) -> CGFloat {
|
||||
|
||||
let rect = self.boundingRect(
|
||||
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
||||
context: nil
|
||||
)
|
||||
|
||||
return ceil(rect.size.height)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +68,7 @@ struct TranslateView: View {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
let translate_url = damus_state.settings.libretranslate_url
|
||||
let api_key = damus_state.settings.libretranslate_api_key
|
||||
|
||||
guard noteLanguage == nil && !checkingTranslationStatus && translate_url != "" else {
|
||||
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,16 +80,22 @@ struct TranslateView: View {
|
||||
currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in.
|
||||
let content = event.get_content(damus_state.keypair.privkey)
|
||||
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
|
||||
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
|
||||
let originalBlocks = event.blocks(damus_state.keypair.privkey)
|
||||
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
|
||||
|
||||
if let lang = noteLanguage, noteLanguage != currentLanguage {
|
||||
// If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language.
|
||||
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
if #available(iOS 16, *) {
|
||||
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
||||
} else {
|
||||
noteLanguage = Locale.canonicalLanguageIdentifier(from: lang)
|
||||
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,15 @@ struct TranslateView: View {
|
||||
if note_lang != currentLanguage {
|
||||
do {
|
||||
// If the note language is different from our language, send a translation request.
|
||||
let translator = Translator(translate_url, apiKey: api_key)
|
||||
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
|
||||
if originalContent == translated_note {
|
||||
// If the translation is the same as the original, don't bother showing it.
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
}
|
||||
} catch {
|
||||
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
|
||||
noteLanguage = currentLanguage
|
||||
@@ -120,8 +127,8 @@ struct TranslateView: View {
|
||||
|
||||
if let translated = translated_note {
|
||||
// Render translated note.
|
||||
let blocks = event.get_blocks(content: translated)
|
||||
translated_artifacts = render_blocks(blocks: blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
let translatedBlocks = event.get_blocks(content: translated)
|
||||
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
checkingTranslationStatus = false
|
||||
@@ -133,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,60 +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
|
||||
}
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: 1000000) 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 {
|
||||
@@ -91,6 +55,10 @@ struct ZapButton: View {
|
||||
return Color.orange
|
||||
}
|
||||
|
||||
if is_charging {
|
||||
return Color.yellow
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return nil
|
||||
}
|
||||
@@ -99,23 +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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,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
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
|
||||
import SwiftUI
|
||||
import Starscream
|
||||
import Kingfisher
|
||||
|
||||
var BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://relay.snort.social",
|
||||
"wss://nostr.bitcoiner.social",
|
||||
"wss://offchain.pub",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.current.fyi",
|
||||
"wss://brb.io",
|
||||
@@ -28,12 +27,16 @@ enum Sheets: Identifiable {
|
||||
case post
|
||||
case report(ReportTarget)
|
||||
case reply(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
case filter
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post: return "post"
|
||||
case .reply(let ev): return "reply-" + ev.id
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .filter: return "filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,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()
|
||||
|
||||
@@ -133,7 +136,37 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
profile_open = false
|
||||
thread_open = false
|
||||
search_open = false
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
var timelineNavItem: some View {
|
||||
VStack {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
case .dms:
|
||||
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
Text("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
.bold()
|
||||
case .search:
|
||||
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
.bold()
|
||||
case .none:
|
||||
Text(verbatim: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,9 +190,10 @@ struct ContentView: View {
|
||||
PostingTimelineView
|
||||
|
||||
case .notifications:
|
||||
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
|
||||
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
}
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
@@ -168,27 +202,12 @@ 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) {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
case .dms:
|
||||
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
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.")
|
||||
.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.")
|
||||
}
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
@@ -245,7 +264,7 @@ struct ContentView: View {
|
||||
if let damus = self.damus_state {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
VStack {
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
@@ -253,7 +272,10 @@ struct ContentView: View {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
.disabled(isSideBarOpened)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -265,19 +287,28 @@ struct ContentView: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Color.clear
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened)
|
||||
)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
.navigationBarHidden(isSideBarOpened ? true: false) // Would prefer a different way of doing this.
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
@@ -287,7 +318,6 @@ struct ContentView: View {
|
||||
}
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
//KingfisherManager.shared.cache.clearDiskCache()
|
||||
setup_notifications()
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
@@ -298,6 +328,17 @@ 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:
|
||||
EventDetailView()
|
||||
case .filter:
|
||||
let timeline = selected_timeline ?? .home
|
||||
if #available(iOS 16.0, *) {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
.presentationDetents([.height(550)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
@@ -416,6 +457,8 @@ struct ContentView: View {
|
||||
let post_res = obj.object as! NostrPostResult
|
||||
switch post_res {
|
||||
case .post(let post):
|
||||
//let post = tup.0
|
||||
//let to_relays = tup.1
|
||||
print("post \(post.content)")
|
||||
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
||||
self.damus_state?.pool.send(.event(new_ev))
|
||||
@@ -520,6 +563,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
self.popToRoot()
|
||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||
|
||||
if timeline == self.selected_timeline {
|
||||
@@ -545,24 +589,34 @@ struct ContentView: View {
|
||||
|
||||
func connect() {
|
||||
let pool = RelayPool()
|
||||
let metadatas = RelayMetadatas()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in BOOTSTRAP_RELAYS {
|
||||
add_relay(pool, relay)
|
||||
if let url = URL(string: relay) {
|
||||
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
self.damus_state = DamusState(pool: pool, keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
tips: TipCounter(our_pubkey: pubkey),
|
||||
profiles: Profiles(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: UserSettingsStore()
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
tips: TipCounter(our_pubkey: pubkey),
|
||||
profiles: Profiles(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts(),
|
||||
events: EventCache()
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
|
||||
@@ -31,6 +31,17 @@ class ActionBarModel: ObservableObject {
|
||||
self.our_zap = our_zap
|
||||
}
|
||||
|
||||
func update(damus: DamusState, evid: String) {
|
||||
self.likes = damus.likes.counts[evid] ?? 0
|
||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
||||
self.our_like = damus.likes.our_events[evid]
|
||||
self.our_boost = damus.boosts.our_events[evid]
|
||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
var is_empty: Bool {
|
||||
return likes == 0 && boosts == 0 && zaps == 0
|
||||
}
|
||||
|
||||
50
damus/Models/BookmarksManager.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ struct DamusState {
|
||||
let zaps: Zaps
|
||||
let lnurls: LNUrls
|
||||
let settings: UserSettingsStore
|
||||
let relay_filters: RelayFilters
|
||||
let relay_metadata: RelayMetadatas
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
@@ -29,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())
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
35
damus/Models/DeepLPlan.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// DeepLPlan.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/3/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DeepLPlan: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var tag: String
|
||||
var displayName: String
|
||||
var url: String
|
||||
}
|
||||
|
||||
case free
|
||||
case pro
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .free:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
|
||||
case .pro:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
|
||||
}
|
||||
}
|
||||
|
||||
static var allModels: [Model] {
|
||||
return Self.allCases.map { $0.model }
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ class DirectMessageModel: ObservableObject {
|
||||
is_request = determine_is_request()
|
||||
}
|
||||
}
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
@@ -31,11 +33,13 @@ class DirectMessageModel: ObservableObject {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
}
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
}
|
||||
}
|
||||
|
||||
13
damus/Models/DraftsModel.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// DraftsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/12/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,7 @@ class FollowersModel: ObservableObject {
|
||||
if has_contact.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
process_contact_event(
|
||||
pool: damus_state.pool,
|
||||
contacts: damus_state.contacts,
|
||||
pubkey: damus_state.pubkey, ev: ev
|
||||
)
|
||||
process_contact_event(state: damus_state, ev: ev)
|
||||
contacts?.append(ev.pubkey)
|
||||
has_contact.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
@@ -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,17 +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, 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)
|
||||
|
||||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert_zap(zap) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,16 +155,13 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard ptag == damus_state.pubkey else {
|
||||
let our_keypair = damus_state.keypair
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) {
|
||||
handle_zap_event_with_zapper(ev, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,7 +175,8 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.handle_zap_event_with_zapper(ev, zapper: zapper)
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,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) {
|
||||
@@ -193,7 +208,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
|
||||
process_contact_event(state: self.damus_state, ev: ev)
|
||||
|
||||
if sub_id == init_subid {
|
||||
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
||||
@@ -213,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)
|
||||
}
|
||||
@@ -229,6 +244,7 @@ class HomeModel: ObservableObject {
|
||||
case .success(let n):
|
||||
let boosted = Counted(event: ev, id: e, total: n)
|
||||
notify(.boosted, boosted)
|
||||
notify(.update_stats, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,14 +254,14 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +302,7 @@ class HomeModel: ObservableObject {
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
// globally handle likes
|
||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||
if !always_process {
|
||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||
return
|
||||
@@ -300,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
|
||||
@@ -356,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,
|
||||
])
|
||||
@@ -366,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,
|
||||
@@ -442,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)
|
||||
}
|
||||
|
||||
@@ -455,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 = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,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))
|
||||
}
|
||||
@@ -643,31 +685,31 @@ func robohash(_ pk: String) -> String {
|
||||
return "https://robohash.org/" + pk
|
||||
}
|
||||
|
||||
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
||||
guard ev.pubkey == pubkey else {
|
||||
func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
guard ev.pubkey == state.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// only use new stuff
|
||||
if let current_ev = contacts.event {
|
||||
if let current_ev = state.contacts.event {
|
||||
guard ev.created_at > current_ev.created_at else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let m_old_ev = contacts.event
|
||||
contacts.event = ev
|
||||
let m_old_ev = state.contacts.event
|
||||
state.contacts.event = ev
|
||||
|
||||
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
}
|
||||
|
||||
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
||||
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
|
||||
add_contact_if_friend(contacts: contacts, ev: ev)
|
||||
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||
load_our_stuff(state: state, ev: ev)
|
||||
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
||||
}
|
||||
|
||||
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let bootstrap_dict: [String: RelayInfo] = [:]
|
||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
|
||||
d[r] = .rw
|
||||
@@ -691,14 +733,15 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
|
||||
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
let new_relay_filters = load_relay_filters(state.pubkey) == nil
|
||||
for d in diff {
|
||||
changed = true
|
||||
if new.contains(d) {
|
||||
if let url = URL(string: d) {
|
||||
try? pool.add_relay(url, info: decoded[d] ?? .rw)
|
||||
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
} else {
|
||||
pool.remove_relay(d)
|
||||
state.pool.remove_relay(d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,14 +750,61 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
|
||||
try? pool.add_relay(url, info: info)
|
||||
|
||||
let relay_id = url.absoluteString
|
||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
metadatas.insert(relay_id: relay_id, metadata: meta)
|
||||
|
||||
// if this is the first time adding filters, we should filter non-paid relays
|
||||
if new_relay_filters && !meta.is_paid {
|
||||
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
|
||||
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
|
||||
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
|
||||
|
||||
var res: (Data, URLResponse)? = nil
|
||||
|
||||
res = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let data = res?.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
|
||||
return nip11
|
||||
}
|
||||
|
||||
func process_relay_metadata() {
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@@ -741,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)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||
var url: String?
|
||||
}
|
||||
|
||||
case none
|
||||
case argosopentech
|
||||
case terraprint
|
||||
case vern
|
||||
@@ -25,8 +24,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
|
||||
case .argosopentech:
|
||||
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||
case .terraprint:
|
||||
|
||||
@@ -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? {
|
||||
|
||||
32
damus/Models/Notifications/EventGroup.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
59
damus/Models/Notifications/ZapGroup.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
298
damus/Models/NotificationsModel.swift
Normal file
@@ -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
|
||||
@@ -20,6 +20,24 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
|
||||
func follows(pubkey: String) -> Bool {
|
||||
guard let contacts = self.contacts else {
|
||||
return false
|
||||
}
|
||||
|
||||
for tag in contacts.tags {
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
continue
|
||||
}
|
||||
|
||||
if tag[1] == pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func get_follow_target() -> FollowTarget {
|
||||
if let contacts = contacts {
|
||||
return .contact(contacts)
|
||||
@@ -70,7 +88,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
|
||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
|
||||
process_contact_event(state: damus, ev: ev)
|
||||
|
||||
// only use new stuff
|
||||
if let current_ev = self.contacts {
|
||||
@@ -93,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,12 +31,14 @@ 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() {
|
||||
loading = true
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
|
||||
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
||||
}
|
||||
|
||||
func unsubscribe(to: String? = nil) {
|
||||
@@ -60,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):
|
||||
@@ -74,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,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 {
|
||||
@@ -111,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()
|
||||
}
|
||||
}
|
||||
@@ -98,6 +99,21 @@ func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tag_is_hashtag(_ tag: [String]) -> Bool {
|
||||
// "hashtag" is deprecated, will remove in the future
|
||||
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
|
||||
}
|
||||
|
||||
func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool {
|
||||
for tag in tags {
|
||||
if tag_is_hashtag(tag) && tag[1] == hashtag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
|
||||
if let hashtags = filter.hashtag {
|
||||
return event_matches_hashtag(ev, hashtags: hashtags)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
damus/Models/TranslationService.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// TranslationService.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/3/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TranslationService: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var tag: String
|
||||
var displayName: String
|
||||
}
|
||||
|
||||
case none
|
||||
case libretranslate
|
||||
case deepl
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
|
||||
case .libretranslate:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
|
||||
case .deepl:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
|
||||
}
|
||||
}
|
||||
|
||||
static var allModels: [Model] {
|
||||
return Self.allCases.map { $0.model }
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,29 @@ func should_show_wallet_selector(_ pubkey: String) -> Bool {
|
||||
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: String, key: String) -> String {
|
||||
return "\(pubkey)_\(key)"
|
||||
}
|
||||
|
||||
func default_zap_setting_key(pubkey: String) -> String {
|
||||
return pk_setting_key(pubkey, key: "default_zap_amount")
|
||||
}
|
||||
|
||||
func set_default_zap_amount(pubkey: String, amount: Int) {
|
||||
let key = default_zap_setting_key(pubkey: pubkey)
|
||||
UserDefaults.standard.setValue(amount, forKey: key)
|
||||
}
|
||||
|
||||
func get_default_zap_amount(pubkey: String) -> Int? {
|
||||
let key = default_zap_setting_key(pubkey: pubkey)
|
||||
let amt = UserDefaults.standard.integer(forKey: key)
|
||||
if amt == 0 {
|
||||
return nil
|
||||
}
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||
let default_wallet = Wallet(rawValue: defaultWalletName)
|
||||
@@ -22,7 +45,23 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return TranslationService(rawValue: translation_service)
|
||||
}
|
||||
|
||||
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
|
||||
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLPlan(rawValue: server_name)
|
||||
}
|
||||
|
||||
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
|
||||
return nil
|
||||
}
|
||||
@@ -30,7 +69,7 @@ func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||
return LibreTranslateServer(rawValue: server_name)
|
||||
}
|
||||
|
||||
func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
||||
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
||||
if let url = server.model.url {
|
||||
return url
|
||||
}
|
||||
@@ -57,6 +96,32 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translation_service: TranslationService {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var deepl_plan: DeepLPlan {
|
||||
didSet {
|
||||
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var deepl_api_key: String {
|
||||
didSet {
|
||||
do {
|
||||
if deepl_api_key == "" {
|
||||
try clearDeepLApiKey()
|
||||
} else {
|
||||
try saveDeepLApiKey(deepl_api_key)
|
||||
}
|
||||
} catch {
|
||||
// No-op.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var libretranslate_server: LibreTranslateServer {
|
||||
didSet {
|
||||
if oldValue == libretranslate_server {
|
||||
@@ -67,7 +132,7 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
libretranslate_api_key = ""
|
||||
|
||||
if libretranslate_server == .custom || libretranslate_server == .none {
|
||||
if libretranslate_server == .custom {
|
||||
libretranslate_url = ""
|
||||
} else {
|
||||
libretranslate_url = libretranslate_server.model.url!
|
||||
@@ -102,18 +167,25 @@ class UserSettingsStore: ObservableObject {
|
||||
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||
|
||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||
|
||||
if let server = get_libretranslate_server(pubkey) {
|
||||
self.libretranslate_server = server
|
||||
self.libretranslate_url = get_libretranslate_url(pubkey, server: server) ?? ""
|
||||
|
||||
// Note from @tyiu:
|
||||
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
|
||||
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
||||
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
||||
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
||||
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
||||
if let translation_service = get_translation_service(pubkey) {
|
||||
self.translation_service = translation_service
|
||||
} else {
|
||||
// Note from @tyiu:
|
||||
// Default server is disabled by default for now until we gain some confidence that it is working well in production.
|
||||
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
||||
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
||||
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
||||
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
||||
libretranslate_server = .none
|
||||
self.translation_service = .none
|
||||
}
|
||||
|
||||
if let libretranslate_server = get_libretranslate_server(pubkey) {
|
||||
self.libretranslate_server = libretranslate_server
|
||||
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
|
||||
} else {
|
||||
// Choose a random server to distribute load.
|
||||
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
|
||||
libretranslate_url = ""
|
||||
}
|
||||
|
||||
@@ -122,15 +194,46 @@ class UserSettingsStore: ObservableObject {
|
||||
} catch {
|
||||
libretranslate_api_key = ""
|
||||
}
|
||||
|
||||
if let deepl_plan = get_deepl_plan(pubkey) {
|
||||
self.deepl_plan = deepl_plan
|
||||
} else {
|
||||
self.deepl_plan = .free
|
||||
}
|
||||
|
||||
do {
|
||||
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
} catch {
|
||||
deepl_api_key = ""
|
||||
}
|
||||
}
|
||||
|
||||
func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||
}
|
||||
|
||||
func clearLibreTranslateApiKey() throws {
|
||||
private func clearLibreTranslateApiKey() throws {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||
}
|
||||
|
||||
private func saveDeepLApiKey(_ apiKey: String) throws {
|
||||
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
}
|
||||
|
||||
private func clearDeepLApiKey() throws {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
}
|
||||
|
||||
func can_translate(_ pubkey: String) -> Bool {
|
||||
switch translation_service {
|
||||
case .none:
|
||||
return false
|
||||
case .libretranslate:
|
||||
return URLComponents(string: libretranslate_url) != nil
|
||||
case .deepl:
|
||||
return deepl_api_key != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||
@@ -138,3 +241,9 @@ struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||
var accessGroup: String? = nil
|
||||
var accountName = "libretranslate_apikey"
|
||||
}
|
||||
|
||||
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
|
||||
var serviceName = "damus"
|
||||
var accessGroup: String? = nil
|
||||
var accountName = "deepl_apikey"
|
||||
}
|
||||
|
||||
83
damus/Models/ZapsModel.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// ZapsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
let target: ZapTarget
|
||||
var zaps: [Zap]
|
||||
|
||||
let zaps_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
init(state: DamusState, target: ZapTarget) {
|
||||
self.state = state
|
||||
self.target = target
|
||||
self.zaps = []
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter.filter_kinds([9735])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
case .note(let note_target):
|
||||
filter.referenced_ids = [note_target.note_id]
|
||||
}
|
||||
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: zaps_subid)
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
|
||||
guard case .nostr_event(let resp) = conn_ev else {
|
||||
return
|
||||
}
|
||||
|
||||
guard resp.subid == zaps_subid else {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ struct Profile: Codable {
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
guard let addr = lud06 ?? lud16 else {
|
||||
guard let addr = lud16 ?? lud06 else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -370,6 +360,7 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
|
||||
|
||||
func encode_json<T: Encodable>(_ val: T) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .withoutEscapingSlashes
|
||||
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
|
||||
}
|
||||
|
||||
@@ -586,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
|
||||
}
|
||||
|
||||
@@ -623,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 {
|
||||
@@ -639,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 {
|
||||
@@ -684,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)
|
||||
@@ -848,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 {
|
||||
|
||||
@@ -24,6 +24,30 @@ enum RelayFlags: Int {
|
||||
case broken = 1
|
||||
}
|
||||
|
||||
struct Limitations: Codable {
|
||||
let payment_required: Bool?
|
||||
|
||||
static var empty: Limitations {
|
||||
Limitations(payment_required: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayMetadata: Codable {
|
||||
let name: String?
|
||||
let description: String?
|
||||
let pubkey: String?
|
||||
let contact: String?
|
||||
let supported_nips: [Int]?
|
||||
let software: String?
|
||||
let version: String?
|
||||
let limitation: Limitations?
|
||||
let payments_url: String?
|
||||
|
||||
var is_paid: Bool {
|
||||
return limitation?.payment_required ?? false
|
||||
}
|
||||
}
|
||||
|
||||
class Relay: Identifiable {
|
||||
let descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
|
||||
@@ -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? {
|
||||
@@ -118,17 +128,16 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
|
||||
}
|
||||
|
||||
func make_nostr_push_event(ev: NostrEvent) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .withoutEscapingSlashes
|
||||
let event_data = try! encoder.encode(ev)
|
||||
let event = String(decoding: event_data, as: UTF8.self)
|
||||
guard let event = encode_json(ev) else {
|
||||
return nil
|
||||
}
|
||||
let encoded = "[\"EVENT\",\(event)]"
|
||||
print(encoded)
|
||||
return encoded
|
||||
}
|
||||
|
||||
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? {
|
||||
@@ -145,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ class RelayPool {
|
||||
var relays: [Relay] = []
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<String> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
|
||||
var descriptors: [RelayDescriptor] {
|
||||
relays.map { $0.descriptor }
|
||||
@@ -149,9 +151,9 @@ class RelayPool {
|
||||
self.send(.unsubscribe(sub_id), to: to)
|
||||
}
|
||||
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)))
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
@@ -193,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) {
|
||||
@@ -241,8 +229,25 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func record_seen(relay_id: String, event: NostrConnectionEvent) {
|
||||
if case .nostr_event(let ev) = event {
|
||||
if case .event(_, let nev) = ev {
|
||||
let k = relay_id + nev.id
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k)
|
||||
if counts[relay_id] == nil {
|
||||
counts[relay_id] = 1
|
||||
} else {
|
||||
counts[relay_id] = (counts[relay_id] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, event: NostrConnectionEvent) {
|
||||
record_last_pong(relay_id: relay_id, event: event)
|
||||
record_seen(relay_id: relay_id, event: event)
|
||||
|
||||
// run req queue when we reconnect
|
||||
if case .ws_event(let ws) = event {
|
||||
|
||||
@@ -143,11 +143,12 @@ 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
|
||||
}
|
||||
guard strBytes.count <= 90 else {
|
||||
guard strBytes.count <= 2024 else {
|
||||
throw Bech32Error.stringLengthExceeded
|
||||
}
|
||||
var lower: Bool = false
|
||||
|
||||
27
damus/Util/Debouncer.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Debouncer.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Debouncer {
|
||||
private let queue = DispatchQueue.main
|
||||
private var workItem: DispatchWorkItem?
|
||||
private var interval: TimeInterval
|
||||
|
||||
init(interval: TimeInterval) {
|
||||
self.interval = interval
|
||||
}
|
||||
|
||||
func debounce(action: @escaping () -> Void) {
|
||||
// Cancel the previous work item if it hasn't yet executed
|
||||
workItem?.cancel()
|
||||
|
||||
// Create a new work item with a delay
|
||||
workItem = DispatchWorkItem { action() }
|
||||
queue.asyncAfter(deadline: .now() + interval, execute: workItem!)
|
||||
}
|
||||
}
|
||||
27
damus/Util/EventCache.swift
Normal file
@@ -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 = [:]
|
||||
}
|
||||
}
|
||||
103
damus/Util/EventHolder.swift
Normal file
@@ -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 = []
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
|
||||
|
||||
}
|
||||
|
||||
func end_editing() {
|
||||
public func end_editing() {
|
||||
UIApplication.shared.connectedScenes
|
||||
.filter {$0.activationState == .foregroundActive}
|
||||
.map {$0 as? UIWindowScene}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
148
damus/Util/KFOptionSetter+.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// KFOptionSetter+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Oleg Abalonski on 2/15/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
extension KFOptionSetter {
|
||||
|
||||
func imageContext(_ imageContext: ImageContext) -> Self {
|
||||
options.callbackQueue = .dispatch(.global(qos: .background))
|
||||
options.processingQueue = .dispatch(.global(qos: .background))
|
||||
options.downloader = CustomImageDownloader.shared
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
|
||||
options.processor = CustomImageProcessor(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
|
||||
options.cacheSerializer = CustomCacheSerializer(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
|
||||
guard let url = fallbackUrl, let key = cacheKey else { return self }
|
||||
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
|
||||
let source = imageResource.convertToSource()
|
||||
options.alternativeSources = [source]
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
let MAX_FILE_SIZE = 20_971_520 // 20MiB
|
||||
|
||||
enum ImageContext {
|
||||
case pfp
|
||||
case banner
|
||||
case note
|
||||
|
||||
func maxMebibyteSize() -> Int {
|
||||
switch self {
|
||||
case .pfp:
|
||||
return 5_242_880 // 5Mib
|
||||
case .banner, .note:
|
||||
return 20_971_520 // 20MiB
|
||||
}
|
||||
}
|
||||
|
||||
func downsampleSize() -> CGSize {
|
||||
switch self {
|
||||
case .pfp:
|
||||
return CGSize(width: 200, height: 200)
|
||||
case .banner:
|
||||
return CGSize(width: 750, height: 250)
|
||||
case .note:
|
||||
return CGSize(width: 500, height: 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomImageProcessor: ImageProcessor {
|
||||
|
||||
let maxSize: Int
|
||||
let downsampleSize: CGSize
|
||||
|
||||
let identifier = "com.damus.customimageprocessor"
|
||||
|
||||
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
|
||||
switch item {
|
||||
case .image(_):
|
||||
// This case will never run
|
||||
return DefaultImageProcessor.default.process(item: item, options: options)
|
||||
case .data(let data):
|
||||
|
||||
// Handle large image size
|
||||
if data.count > maxSize {
|
||||
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
|
||||
}
|
||||
|
||||
// Handle SVG image
|
||||
if let dataString = String(data: data, encoding: .utf8),
|
||||
let svg = SVG(dataString) {
|
||||
|
||||
let render = UIGraphicsImageRenderer(size: svg.size)
|
||||
let image = render.image { context in
|
||||
svg.draw(in: context.cgContext)
|
||||
}
|
||||
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
}
|
||||
|
||||
return DefaultImageProcessor.default.process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomCacheSerializer: CacheSerializer {
|
||||
|
||||
let maxSize: Int
|
||||
let downsampleSize: CGSize
|
||||
|
||||
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
|
||||
return DefaultCacheSerializer.default.data(with: image, original: original)
|
||||
}
|
||||
|
||||
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
|
||||
if data.count > maxSize {
|
||||
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
|
||||
}
|
||||
|
||||
return DefaultCacheSerializer.default.image(with: data, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSessionDelegate: SessionDelegate {
|
||||
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
let contentLength = response.expectedContentLength
|
||||
|
||||
// Content-Length header is optional (-1 when missing)
|
||||
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
|
||||
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomImageDownloader: ImageDownloader {
|
||||
|
||||
static let shared = CustomImageDownloader(name: "shared")
|
||||
|
||||
override init(name: String) {
|
||||
super.init(name: name)
|
||||
sessionDelegate = CustomSessionDelegate()
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,20 @@ func get_saved_privkey() -> String? {
|
||||
return mkey.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
}
|
||||
|
||||
/**
|
||||
Detects whether a string might contain an nsec1 prefixed private key.
|
||||
It does not determine if it's the current user's private key and does not verify if it is properly encoded or has the right length.
|
||||
*/
|
||||
func contentContainsPrivateKey(_ content: String) -> Bool {
|
||||
if #available(iOS 16.0, *) {
|
||||
return content.contains(/nsec1[02-9ac-z]+/)
|
||||
} else {
|
||||
let regex = try! NSRegularExpression(pattern: "nsec1[02-9ac-z]+")
|
||||
return (regex.firstMatch(in: content, range: NSRange(location: 0, length: content.count)) != nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate func removePrivateKeyFromUserDefaults() throws {
|
||||
guard let privKey = UserDefaults.standard.string(forKey: "privkey") else { return }
|
||||
try save_privkey(privkey: privKey)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
17
damus/Util/LocalizationUtil.swift
Normal file
@@ -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
|
||||
|
||||
26
damus/Util/NIPURLBuilder.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// NIPURLBuilder.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Honk on 2/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NIPURLBuilder {
|
||||
static private let baseURL = "https://github.com/nostr-protocol/nips/blob/master/"
|
||||
static func url(forNIP nip: Int) -> URL? {
|
||||
let urlString = baseURL + "\(formatNipNumber(nip: nip)).md"
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
static func formatNipNumber(nip: Int) -> String {
|
||||
let formatted: String
|
||||
if nip < 10 {
|
||||
formatted = "0\(nip)"
|
||||
} else {
|
||||
formatted = "\(nip)"
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,15 @@ extension Notification.Name {
|
||||
static var deleted_account: Notification.Name {
|
||||
return Notification.Name("deleted_account")
|
||||
}
|
||||
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 {
|
||||
|
||||