Compare commits

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

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

Changelog-Fixed: Fix lag on startup when you have lots of DMs
Changelog-Fixed: Fix an issues where dm notifications appear without any new events
2023-02-15 11:14:13 -08:00
William Casarin cf90480501 perf: decode large events in background threads
should help with hitches a bit
2023-02-15 11:14:13 -08:00
OlegAbaandWilliam Casarin f0075904c2 Fix frequent KFImage hang
Changelog-Fixed: Fix some hangs when scrolling by images
Closes: #614
2023-02-15 11:13:04 -08:00
tyiu a41acc12e7 Import translations 2023-02-15 12:50:57 -05:00
William Casarin 1e22984d52 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 08:49:45 -08:00
tyiuandWilliam Casarin 9080e4efae Force default zap amount text field to accept only numbers
Changelog-Fixed: Force default zap amount text field to accept only numbers
Closes: #612
2023-02-15 08:47:22 -08:00
tyiu 6488634eda Import translations 2023-02-15 10:54:45 -05:00
tyiu 355cd1283c Wrap non-translatable strings so that they do not get exported 2023-02-15 10:44:44 -05:00
William Casarin 6ed9c408f9 v1.1.0-2 changelog 2023-02-14 10:17:08 -08:00
William Casarin 5f52e6f62f v1.1.0-2 2023-02-14 10:15:40 -08:00
William Casarin 59211bb4fd Ensure stats get updated in realtime on action bars
Changelog-Fixed: Ensure stats get updated in realtime on action bars
2023-02-14 10:05:59 -08:00
William Casarin 6d634763c5 Fix repost counters
Changelog-Fixed: Fix reposts not getting counted properly
2023-02-14 10:05:19 -08:00
William Casarin 49cf56f4c2 Show other people's zaps
Changelog-Fixed: Fix a bug where zaps on other people's posts weren't showing
2023-02-13 17:50:50 -08:00
William Casarin 98c7bf5afc Revert "Sidebar Fixes"
This reverts commit 6653798d27.
2023-02-13 16:51:13 -08:00
tyiuandWilliam Casarin 70a7239cfd Show app version at bottom of ConfigView 2023-02-13 10:07:43 -08:00
William Casarin 0e83632896 Revert "Add the ability to unlike posts"
This reverts commit 237c939639.
2023-02-13 10:04:31 -08:00
Gert GoetandWilliam Casarin f0df4aa218 Strip common punctuations from URLs
Changelog-Fixed: Fix punctuation getting included in some urls
Closes: #575
2023-02-13 10:03:35 -08:00
Gert GoetandWilliam Casarin bb9fc6f905 Always stop at first whitespace 2023-02-13 10:03:17 -08:00
tyiuandWilliam Casarin f69e0c660a Fix language detection to look at only text and not URLs or hashtags
Changelog-Fixed: Improve language detection
Closes: #577
2023-02-13 09:56:26 -08:00
William Casarin 9089246b6b Merge translations 2023-02-13 09:55:25 -08:00
Oleg GordiichukandWilliam Casarin 237c939639 Add the ability to unlike posts
Changelog-Added: Add ability to unlike posts
Closes: #580
2023-02-13 09:50:14 -08:00
William Casarin 4f86361b63 Revert "Improved blur on images, especially in dark mode"
Can't open images anymore

This reverts commit 47a6f7ff38.
2023-02-13 09:46:45 -08:00
William Casarin 209ad71ff3 Update kingfisher to potentially fix some crashes
Changelog-Fixed: Fix some animated image crashes
2023-02-13 09:40:02 -08:00
William Casarin c728850524 Refactor drafts 2023-02-13 09:39:56 -08:00
tyiuandWilliam Casarin bc638f79f6 Add saved drafts to posts, replies, and DMs
Changelog-Added: Save drafts to posts, replies and DMs
Closes: #582
2023-02-13 09:39:47 -08:00
Bryan MontzandWilliam Casarin 47a6f7ff38 Improved blur on images, especially in dark mode
Changelog-Changed: Improve blur on images, especially in dark mode
Closes: #583
2023-02-13 09:14:54 -08:00
Ben WeeksandWilliam Casarin 6653798d27 Sidebar Fixes
Closes: #587
2023-02-13 09:13:51 -08:00
William Casarin e9ea96ffb6 Bump to v1.1.0 2023-02-13 09:13:45 -08:00
tyiu 59ccde9c38 Import translations 2023-02-13 11:43:06 -05:00
tyiu f9be7b166c Export source translations 2023-02-12 15:01:49 -05:00
OlegAba 2366089896 Fix vertical spacing bug 2023-02-10 16:52:34 -05:00
William Casarin 4f2bacfaab Configurable zap amount 2023-02-10 13:52:27 -08:00
William Casarin 5a8b29b5cc Fix changelog 2023-02-10 13:20:38 -08:00
OlegAba 9a95967a81 Refactor pfp image view to use zoomable scroll view 2023-02-10 01:03:55 -05:00
OlegAba 504108da75 Add custom profile navbar 2023-02-09 18:24:16 -05:00
OlegAba d43a2ff92d Move safeAreaInset ref to Theme 2023-02-09 18:22:48 -05:00
177 changed files with 4037 additions and 24475 deletions
+53 -3
View File
@@ -1,7 +1,59 @@
## [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 ## [1.0.0-15] - 2023-02-10
### Added ### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu) - Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu) - Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin) - Show if relay is paid (William Casarin)
@@ -13,7 +65,6 @@
- Use local authentication (faceid) to access private key (Andrii Sievrikov) - Use local authentication (faceid) to access private key (Andrii Sievrikov)
- Add accessibility labels to action bar (Bryan Montz) - Add accessibility labels to action bar (Bryan Montz)
- Copy invoice button (Joel Klabo) - Copy invoice button (Joel Klabo)
- Ability to change remote image loading policy (radixrat)
- Receive Lightning Zaps (William Casarin) - Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan) - Allow text selection in bio (Suhail Saqan)
@@ -23,10 +74,8 @@
- Show "Follow Back" button on profile page (William Casarin) - Show "Follow Back" button on profile page (William Casarin)
- When on your profile page, open relay view instead for your own relays (Terry Yiu) - When on your profile page, open relay view instead for your own relays (Terry Yiu)
- Updated QR code view, include profile image, etc (ericholguin) - Updated QR code view, include profile image, etc (ericholguin)
- Make app smaller by optimizing pngs (pea-sys)
- Clicking relay numbers now goes to relay config (radixrat) - Clicking relay numbers now goes to relay config (radixrat)
### Fixed ### Fixed
- Load zaps, likes and reposts when you open a thread (William Casarin) - Load zaps, likes and reposts when you open a thread (William Casarin)
@@ -561,3 +610,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2 [0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
-18
View File
@@ -108,24 +108,6 @@ All user-facing strings must have a comment in order to provide context to trans
[transifex]: https://explore.transifex.com/damus/damus-ios/ [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 ### Awards
There may be nostr badges awarded for contributors in the future... :) There may be nostr badges awarded for contributors in the future... :)
+9 -2
View File
@@ -26,6 +26,10 @@ static inline int is_boundary(char c) {
return !isalnum(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) static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{ {
c->start = content; c->start = content;
@@ -55,8 +59,8 @@ static int consume_until_whitespace(struct cursor *cur, int or_end) {
while (cur->p < cur->end) { while (cur->p < cur->end) {
c = *cur->p; c = *cur->p;
if (is_whitespace(c) && consumedAtLeastOne) if (is_whitespace(c))
return 1; return consumedAtLeastOne;
cur->p++; cur->p++;
consumedAtLeastOne = true; consumedAtLeastOne = true;
@@ -221,6 +225,9 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 0; return 0;
} }
// strip any unwanted characters
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
block->type = BLOCK_URL; block->type = BLOCK_URL;
block->block.str.start = (const char *)start; block->block.str.start = (const char *)start;
block->block.str.end = (const char *)cur->p; block->block.str.end = (const char *)cur->p;
+69 -12
View File
@@ -15,6 +15,7 @@
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.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 */; }; 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; }; 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; }; 3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
@@ -41,6 +42,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; }; 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; };
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; }; 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; }; 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; }; 4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; }; 4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; }; 4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -154,6 +156,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.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 */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; }; 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -197,6 +202,7 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; }; 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; }; 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.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 */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; }; 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
@@ -204,9 +210,10 @@
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; 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 */; }; 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.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 */; }; 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
@@ -216,7 +223,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; }; F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; }; F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; }; F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; }; F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -248,6 +255,9 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.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>"; }; 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>"; }; 3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -258,6 +268,12 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -270,6 +286,7 @@
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -310,6 +327,7 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -453,6 +471,9 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -499,15 +520,17 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; 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>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
@@ -517,7 +540,7 @@
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.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>"; }; 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>"; }; F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; }; F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -653,13 +676,13 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */, BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */, 4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */, 4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */, 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */, 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */, 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */, 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */, 4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -667,6 +690,7 @@
4C75EFA227FA576C0006080F /* Views */ = { 4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */, 4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */, 4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */, 4CAAD8AE29888A9B00060CEA /* Relays */,
@@ -705,7 +729,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */, 4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */, 4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */, 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */, F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */, 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */, 4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */, 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -777,6 +801,9 @@
4CB883A72975FC1800DC99E7 /* Zaps.swift */, 4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */, 4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */, 3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -839,6 +866,15 @@
path = Events; path = Events;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = { 4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -857,6 +893,7 @@
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */, 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */, 4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */, 4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -916,6 +953,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = { 4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */, DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */, 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */, 4C363A9F2828A8DD006E126D /* LikeTests.swift */,
@@ -1113,6 +1151,9 @@
"zh-CN", "zh-CN",
"el-GR", "el-GR",
ja, ja,
id,
cs,
ru,
); );
mainGroup = 4CE6DEDA27F7A08100C66700; mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = ( packageReferences = (
@@ -1204,9 +1245,11 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */, 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */, 4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */, 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */, F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1243,7 +1286,6 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */, 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */, 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */, 4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */, 4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1251,6 +1293,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */, BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */, 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */, 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
@@ -1274,6 +1317,7 @@
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
@@ -1324,6 +1368,7 @@
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -1339,6 +1384,7 @@
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */, 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */, 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
@@ -1352,6 +1398,7 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
); );
@@ -1370,6 +1417,7 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */, 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */, 4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */, 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */, 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */, 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
); );
@@ -1417,6 +1465,9 @@
3A5CAE1F298DC0DB00B5334F /* zh-CN */, 3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */, 3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */, 3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
); );
name = Localizable.stringsdict; name = Localizable.stringsdict;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1437,6 +1488,9 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */, 3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */, 3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */, 3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1457,6 +1511,9 @@
3A5CAE1E298DC0DB00B5334F /* zh-CN */, 3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */, 3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */, 3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
); );
name = Localizable.strings; name = Localizable.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1592,7 +1649,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1615,7 +1672,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1634,7 +1691,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1657,7 +1714,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher", "location" : "https://github.com/onevcat/Kingfisher",
"state" : { "state" : {
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac", "revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.4.0" "version" : "7.6.1"
} }
}, },
{ {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -11,24 +11,6 @@
} }
}, },
"idiom" : "universal" "idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
} }
], ],
"info" : { "info" : {
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-copy.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

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

Before

Width:  |  Height:  |  Size: 400 B

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

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

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

Before

Width:  |  Height:  |  Size: 950 B

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

Before

Width:  |  Height:  |  Size: 252 B

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

Before

Width:  |  Height:  |  Size: 458 B

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

Before

Width:  |  Height:  |  Size: 671 B

+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "lnlink.png", "filename" : "lnlink.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "damus-nobg.png", "filename" : "damus-nobg.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "muun.png", "filename" : "muun.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "phoenix.png", "filename" : "phoenix.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "river.png", "filename" : "river.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "strike.png", "filename" : "strike.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "undercover.png", "filename" : "undercover.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "walletofsatoshi.png", "filename" : "walletofsatoshi.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "zebedee.png", "filename" : "zebedee.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [ "images" : [
{ {
"filename" : "zeus.png", "filename" : "zeus.png",
"idiom" : "universal", "idiom" : "universal"
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
+11 -45
View File
@@ -66,20 +66,11 @@ struct ImageContextMenuModifier: ViewModifier {
private struct ImageContainerView: View { private struct ImageContainerView: View {
@ObservedObject var imageModel: KFImageModel let url: URL?
@State private var image: UIImage? @State private var image: UIImage?
@State private var showShareSheet = false @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 { private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage? @Binding var handler: UIImage?
@@ -91,30 +82,17 @@ private struct ImageContainerView: View {
var body: some View { var body: some View {
KFAnimatedImage(imageModel.url) KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background))) .imageContext(.note)
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.configure { view in .configure { view in
view.framePreloadCount = 1 view.framePreloadCount = 3
} }
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image)) .imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped() .clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet)) .modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $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 private var selectedIndex = 0
@State var showMenu = true @State var showMenu = true
var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
var navBarView: some View { var navBarView: some View {
VStack { VStack {
HStack { HStack {
@@ -180,8 +150,8 @@ struct ImageView: View {
ZoomableScrollView { ZoomableScrollView {
ImageContainerView(url: urls[index]) ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top) .padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom) .padding(.bottom, Theme.safeAreaInsets?.bottom)
} }
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
@@ -210,7 +180,7 @@ struct ImageView: View {
} }
} }
.animation(.easeInOut, value: showMenu) .animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom) .padding(.bottom, Theme.safeAreaInsets?.bottom)
) )
} }
} }
@@ -229,12 +199,8 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear) .foregroundColor(Color.clear)
.overlay { .overlay {
KFAnimatedImage(url) KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background))) .imageContext(.note)
.processingQueue(.dispatch(.global(qos: .background))) .cancelOnDisappear(true)
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.configure { view in .configure { view in
view.framePreloadCount = 3 view.framePreloadCount = 3
} }
+96
View File
@@ -0,0 +1,96 @@
//
// SelectableText.swift
// damus
//
// Created by Oleg Abalonski on 2/16/23.
//
import UIKit
import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.onAppear {
self.selectedTextWidth = geo.size.width
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width
}
}
.frame(height: selectedTextHeight)
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
DispatchQueue.main.async {
height = newHeight
}
}
func createNSAttributedString() -> NSMutableAttributedString {
let mutableAttributedString = NSMutableAttributedString(attributedString)
let myAttribute = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: textColor
]
mutableAttributedString.addAttributes(
myAttribute,
range: NSRange.init(location: 0, length: mutableAttributedString.length)
)
return mutableAttributedString
}
}
fileprivate extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.size.height)
}
}
+2 -5
View File
@@ -11,7 +11,6 @@ import NaturalLanguage
struct TranslateView: View { struct TranslateView: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false @State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en" @State var currentLanguage: String = "en"
@@ -34,9 +33,7 @@ struct TranslateView: View {
} }
.translate_button_style() .translate_button_style()
Text(artifacts.content) SelectableText(attributedString: artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
} }
} }
@@ -143,6 +140,6 @@ struct TranslateView: View {
struct TranslateView_Previews: PreviewProvider { struct TranslateView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let ds = test_damus_state() let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .selected) TranslateView(damus_state: ds, event: test_event)
} }
} }
+4 -5
View File
@@ -52,8 +52,8 @@ struct ZapButton: View {
damus_state.lnurls.endpoints[target.pubkey] = payreq damus_state.lnurls.endpoints[target.pubkey] = payreq
} }
let tip_amount = get_default_tip_amount(pubkey: damus_state.pubkey) let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: tip_amount) else { guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
DispatchQueue.main.async { DispatchQueue.main.async {
zapping = false zapping = false
} }
@@ -100,7 +100,7 @@ struct ZapButton: View {
} }
var body: some View { var body: some View {
ZStack { HStack(spacing: 4) {
EventActionButton(img: zap_img, col: zap_color) { EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped { if bar.zapped {
//notify(.delete, bar.our_tip) //notify(.delete, bar.our_tip)
@@ -110,8 +110,7 @@ struct ZapButton: View {
} }
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")") Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")"))
.offset(x: 22)
.font(.footnote) .font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray) .foregroundColor(bar.zapped ? Color.orange : Color.gray)
} }
+22 -20
View File
@@ -92,7 +92,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies @State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel() @StateObject var home: HomeModel = HomeModel()
// connect retry timer // connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -136,7 +136,7 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack { ZStack {
if let damus = self.damus_state { if let damus = self.damus_state {
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter) TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
} }
} }
} }
@@ -163,7 +163,7 @@ struct ContentView: View {
Text("Notifications", comment: "Toolbar label for Notifications view.") Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold() .bold()
case .search: case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.") Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold() .bold()
case .none: case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.") Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
@@ -192,7 +192,7 @@ struct ContentView: View {
case .notifications: case .notifications:
VStack(spacing: 0) { VStack(spacing: 0) {
Divider() Divider()
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true }) TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
} }
case .dms: case .dms:
DirectMessagesView(damus_state: damus_state!) DirectMessagesView(damus_state: damus_state!)
@@ -202,7 +202,7 @@ struct ContentView: View {
EmptyView() 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 { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
timelineNavItem timelineNavItem
@@ -295,7 +295,7 @@ struct ContentView: View {
self.active_sheet = .filter self.active_sheet = .filter
}) { }) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease // checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label("Filter", systemImage: "line.3.horizontal.decrease") Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray) .foregroundColor(.gray)
//.contentShape(Rectangle()) //.contentShape(Rectangle())
} }
@@ -328,7 +328,7 @@ struct ContentView: View {
PostView(replying_to: nil, references: [], damus_state: damus_state!) PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event): case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!) ReplyView(replying_to: event, damus: damus_state!)
case .event(let event): case .event:
EventDetailView() EventDetailView()
case .filter: case .filter:
let timeline = selected_timeline ?? .home let timeline = selected_timeline ?? .home
@@ -601,19 +601,21 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: home.handle_event) pool.register_handler(sub_id: sub_id, handler: home.handle_event)
self.damus_state = DamusState(pool: pool, keypair: keypair, self.damus_state = DamusState(pool: pool,
likes: EventCounter(our_pubkey: pubkey), keypair: keypair,
boosts: EventCounter(our_pubkey: pubkey), likes: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey),
profiles: Profiles(), tips: TipCounter(our_pubkey: pubkey),
dms: home.dms, profiles: Profiles(),
previews: PreviewCache(), dms: home.dms,
zaps: Zaps(our_pubkey: pubkey), previews: PreviewCache(),
lnurls: LNUrls(), zaps: Zaps(our_pubkey: pubkey),
settings: UserSettingsStore(), lnurls: LNUrls(),
relay_filters: relay_filters, settings: UserSettingsStore(),
relay_metadata: metadatas relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts()
) )
home.damus_state = self.damus_state! home.damus_state = self.damus_state!
+11
View File
@@ -31,6 +31,17 @@ class ActionBarModel: ObservableObject {
self.our_zap = our_zap 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 { var is_empty: Bool {
return likes == 0 && boosts == 0 && zaps == 0 return likes == 0 && boosts == 0 && zaps == 0
} }
+2 -1
View File
@@ -23,6 +23,7 @@ struct DamusState {
let settings: UserSettingsStore let settings: UserSettingsStore
let relay_filters: RelayFilters let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas let relay_metadata: RelayMetadatas
let drafts: Drafts
var pubkey: String { var pubkey: String {
return keypair.pubkey return keypair.pubkey
@@ -34,6 +35,6 @@ struct DamusState {
static var empty: DamusState { static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas()) 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())
} }
} }
+4
View File
@@ -13,6 +13,8 @@ class DirectMessageModel: ObservableObject {
is_request = determine_is_request() is_request = determine_is_request()
} }
} }
@Published var draft: String
var is_request: Bool var is_request: Bool
var our_pubkey: String var our_pubkey: String
@@ -31,11 +33,13 @@ class DirectMessageModel: ObservableObject {
self.events = events self.events = events
self.is_request = false self.is_request = false
self.our_pubkey = our_pubkey self.our_pubkey = our_pubkey
self.draft = ""
} }
init(our_pubkey: String) { init(our_pubkey: String) {
self.events = [] self.events = []
self.is_request = false self.is_request = false
self.our_pubkey = our_pubkey self.our_pubkey = our_pubkey
self.draft = ""
} }
} }
+13
View File
@@ -0,0 +1,13 @@
//
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
//
import Foundation
class Drafts: ObservableObject {
@Published var post: String = ""
@Published var replies: [NostrEvent: String] = [:]
}
+56 -2
View File
@@ -9,9 +9,63 @@ import Foundation
class EventsModel: ObservableObject { 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] = [] @Published var events: [NostrEvent] = []
init() { init(state: DamusState, target: String, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([kind.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
state.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
return
}
guard last_etag(tags: ev.tags) == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state)
}
} }
} }
+83 -33
View File
@@ -38,6 +38,9 @@ class HomeModel: ObservableObject {
var channels: [String: NostrEvent] = [:] var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:] var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false 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 home_subid = UUID().description
let contacts_subid = UUID().description let contacts_subid = UUID().description
@@ -47,25 +50,37 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits() @Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = [] @Published var notifications: EventHolder
@Published var dms: DirectMessagesModel @Published var dms: DirectMessagesModel
@Published var events: [NostrEvent] = [] @Published var events: EventHolder
@Published var loading: Bool = false @Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel() @Published var signal: SignalModel = SignalModel()
init() { init() {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = DamusState.empty self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.dms = DirectMessagesModel(our_pubkey: "")
} }
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = damus_state self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
} }
var pool: RelayPool { var pool: RelayPool {
return damus_state.pool 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 { func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) { if !has_event.keys.contains(sub_id) {
@@ -128,7 +143,7 @@ class HomeModel: ObservableObject {
return return
} }
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { if !notifications.insert(ev) {
return return
} }
@@ -142,12 +157,12 @@ class HomeModel: ObservableObject {
return return
} }
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) { if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper) handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
return return
} }
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else { guard let profile = damus_state.profiles.lookup(id: ptag) else {
return return
} }
@@ -180,9 +195,9 @@ class HomeModel: ObservableObject {
} }
func filter_muted() { 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.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) { func handle_delete_event(_ ev: NostrEvent) {
@@ -230,6 +245,7 @@ class HomeModel: ObservableObject {
case .success(let n): case .success(let n):
let boosted = Counted(event: ev, id: e, total: n) let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted) notify(.boosted, boosted)
notify(.update_stats, e)
} }
} }
@@ -247,6 +263,7 @@ class HomeModel: ObservableObject {
case .success(let n): case .success(let n):
let liked = Counted(event: ev, id: e.ref_id, total: n) let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked) notify(.liked, liked)
notify(.update_stats, e.ref_id)
} }
} }
@@ -287,7 +304,7 @@ class HomeModel: ObservableObject {
switch ev { switch ev {
case .event(let sub_id, let ev): case .event(let sub_id, let ev):
// globally handle likes // 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 { if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we? // TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return return
@@ -301,10 +318,11 @@ class HomeModel: ObservableObject {
case .eose(let sub_id): case .eose(let sub_id):
if sub_id == dms_subid { if sub_id == dms_subid {
let dms = dms.dms.flatMap { $0.1.events } var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
} else if sub_id == notifications_subid { } else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state)
} }
self.loading = false self.loading = false
@@ -443,10 +461,10 @@ class HomeModel: ObservableObject {
return return
} }
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { if !notifications.insert(ev) {
return return
} }
handle_last_event(ev: ev, timeline: .notifications) handle_last_event(ev: ev, timeline: .notifications)
} }
@@ -456,12 +474,10 @@ class HomeModel: ObservableObject {
} }
} }
func insert_home_event(_ ev: NostrEvent) -> Bool { func insert_home_event(_ ev: NostrEvent) {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at }) if events.insert(ev) {
if ok {
handle_last_event(ev: ev, timeline: .home) handle_last_event(ev: ev, timeline: .home)
} }
return ok
} }
func handle_text_event(sub_id: String, _ ev: NostrEvent) { func handle_text_event(sub_id: String, _ ev: NostrEvent) {
@@ -470,15 +486,33 @@ class HomeModel: ObservableObject {
} }
if sub_id == home_subid { if sub_id == home_subid {
let _ = insert_home_event(ev) insert_home_event(ev)
} else if sub_id == notifications_subid { } else if sub_id == notifications_subid {
handle_notification(ev: ev) handle_notification(ev: ev)
} }
} }
func handle_dm(_ ev: NostrEvent) { 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) { guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
self.new_events = notifs return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
} }
} }
} }
@@ -624,14 +658,14 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap // load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey) let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if let _ = URL(string: picture) { if URL(string: picture) != nil {
DispatchQueue.main.async { DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
} }
} }
let banner = tprof.profile.banner ?? "" let banner = tprof.profile.banner ?? ""
if let _ = URL(string: banner) { if URL(string: banner) != nil {
DispatchQueue.main.async { DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
} }
@@ -759,14 +793,11 @@ func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
func process_relay_metadata() { func process_relay_metadata() {
} }
func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? { @discardableResult
// hide blocked users func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
guard should_show_event(contacts: contacts, ev: ev) else {
return prev_events
}
var inserted = false var inserted = false
var found = false var found = false
let ours = ev.pubkey == our_pubkey let ours = ev.pubkey == our_pubkey
var i = 0 var i = 0
@@ -793,15 +824,34 @@ func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: Dir
} }
if !found { if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey) let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model)) 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 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 { 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 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 return a.1.events.last!.created_at > b.1.events.last!.created_at
} }
-114
View File
@@ -1,114 +0,0 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import UIKit
import Kingfisher
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
+27 -1
View File
@@ -211,6 +211,32 @@ enum Amount: Equatable {
} }
} }
func format_actions_abbrev(_ actions: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
if actions >= 1_000_000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if actions >= 1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return "\(actions)"
}
let actions = NSNumber(value: actions)
return formatter.string(from: actions) ?? "\(actions)"
}
func format_msats_abbrev(_ msats: Int64) -> String { func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter() let formatter = NumberFormatter()
formatter.numberStyle = .decimal formatter.numberStyle = .decimal
@@ -247,7 +273,7 @@ func format_msats(_ msat: Int64) -> String {
let sats = NSNumber(value: (Double(msat) / 1000.0)) let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats) return String(format: Bundle.main.localizedString(forKey: "sats_count", value: nil, table: nil), sats.decimalValue as NSDecimalNumber, formattedSats)
} }
func convert_invoice_block(_ b: invoice_block) -> Block? { func convert_invoice_block(_ b: invoice_block) -> Block? {
+4 -2
View File
@@ -8,7 +8,7 @@
import Foundation import Foundation
class ProfileModel: ObservableObject, Equatable { class ProfileModel: ObservableObject, Equatable {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var contacts: NostrEvent? = nil @Published var contacts: NostrEvent? = nil
@Published var following: Int = 0 @Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil @Published var relays: [String: RelayInfo]? = nil
@@ -111,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable {
return return
} }
if ev.is_textlike || ev.known_kind == .boost { 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 { } else if ev.known_kind == .contacts {
handle_profile_contact_event(ev) handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata { } else if ev.known_kind == .metadata {
+3 -65
View File
@@ -8,71 +8,9 @@
import Foundation import Foundation
class ReactionsModel: ObservableObject { final class ReactionsModel: EventsModel {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
@Published var reactions: [NostrEvent] init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .like)
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
}
} }
} }
+4 -66
View File
@@ -7,71 +7,9 @@
import Foundation import Foundation
class RepostsModel: ObservableObject { final class RepostsModel: EventsModel {
let state: DamusState
let target: String init(state: DamusState, target: String) {
let sub_id: String super.init(state: state, target: target, kind: .boost)
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
}
} }
} }
+6 -5
View File
@@ -10,7 +10,7 @@ import Foundation
/// The data model for the SearchHome view, typically something global-like /// The data model for the SearchHome view, typically something global-like
class SearchHomeModel: ObservableObject { class SearchHomeModel: ObservableObject {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var loading: Bool = false @Published var loading: Bool = false
var seen_pubkey: Set<String> = Set() var seen_pubkey: Set<String> = Set()
@@ -31,7 +31,8 @@ class SearchHomeModel: ObservableObject {
} }
func filter_muted() { 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() { func subscribe() {
@@ -61,8 +62,8 @@ class SearchHomeModel: ObservableObject {
} }
seen_pubkey.insert(ev.pubkey) seen_pubkey.insert(ev.pubkey)
let _ = insert_uniq_sorted_event(events: &events, new_ev: ev) { if self.events.insert(ev) {
$0.created_at > $1.created_at self.objectWillChange.send()
} }
} }
case .notice(let msg): case .notice(let msg):
@@ -75,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime // global events are not realtime
unsubscribe(to: relay_id) unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state)
} }
+4 -3
View File
@@ -9,7 +9,7 @@ import Foundation
class SearchModel: ObservableObject { class SearchModel: ObservableObject {
@Published var events: [NostrEvent] = [] var events: EventHolder = EventHolder()
@Published var loading: Bool = false @Published var loading: Bool = false
@Published var channel_name: String? = nil @Published var channel_name: String? = nil
@@ -26,7 +26,8 @@ class SearchModel: ObservableObject {
} }
func filter_muted() { 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() { func subscribe() {
@@ -57,7 +58,7 @@ class SearchModel: ObservableObject {
return 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() objectWillChange.send()
} }
} }
+13 -6
View File
@@ -16,15 +16,22 @@ func pk_setting_key(_ pubkey: String, key: String) -> String {
return "\(pubkey)_\(key)" return "\(pubkey)_\(key)"
} }
let tip_amount_key = "default_tip_amount" func default_zap_setting_key(pubkey: String) -> String {
func set_default_tip_amount(pubkey: String, amount: Int64) { return pk_setting_key(pubkey, key: "default_zap_amount")
let key = pk_setting_key(pubkey, key: tip_amount_key) }
func set_default_zap_amount(pubkey: String, amount: Int) {
let key = default_zap_setting_key(pubkey: pubkey)
UserDefaults.standard.setValue(amount, forKey: key) UserDefaults.standard.setValue(amount, forKey: key)
} }
func get_default_tip_amount(pubkey: String) -> Int64 { func get_default_zap_amount(pubkey: String) -> Int? {
let key = "\(pubkey)_\(tip_amount_key)" let key = default_zap_setting_key(pubkey: pubkey)
return UserDefaults.standard.object(forKey: key) as? Int64 ?? 1000000 let amt = UserDefaults.standard.integer(forKey: key)
if amt == 0 {
return nil
}
return amt
} }
+2 -16
View File
@@ -278,21 +278,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return (self.flags & 1) != 0 return (self.flags & 1) != 0
} }
init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) { init(id: String = "", 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]] = []) {
self.id = id self.id = id
self.sig = "" self.sig = ""
@@ -300,7 +286,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
self.pubkey = pubkey self.pubkey = pubkey
self.kind = kind self.kind = kind
self.tags = tags self.tags = tags
self.created_at = Int64(Date().timeIntervalSince1970) self.created_at = createdAt
} }
init(from: NostrEvent, content: String? = nil) { init(from: NostrEvent, content: String? = nil) {
+1 -1
View File
@@ -72,7 +72,7 @@ func char_to_hex(_ c: UInt8) -> UInt8?
return nil; return nil;
} }
@discardableResult
func hex_decode(_ str: String) -> [UInt8]? func hex_decode(_ str: String) -> [UInt8]?
{ {
if str.count == 0 { if str.count == 0 {
+40 -37
View File
@@ -13,42 +13,41 @@ enum NostrConnectionEvent {
case nostr_event(NostrResponse) case nostr_event(NostrResponse)
} }
class RelayConnection: WebSocketDelegate { final class RelayConnection: WebSocketDelegate {
var isConnected: Bool = false private(set) var isConnected = false
var isConnecting: Bool = false private(set) var isConnecting = false
var isReconnecting: Bool = false private(set) var isReconnecting = false
var last_connection_attempt: Double = 0
var socket: WebSocket private(set) var last_connection_attempt: TimeInterval = 0
var handleEvent: (NostrConnectionEvent) -> () private lazy var socket = {
let url: URL 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) -> ()) { init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
self.url = url self.url = url
self.handleEvent = handleEvent self.handleEvent = handleEvent
// just init, we don't actually use this one
self.socket = make_websocket(url: url)
} }
func reconnect() { func reconnect() {
if self.isConnected { if isConnected {
self.isReconnecting = true isReconnecting = true
self.disconnect() disconnect()
} else { } else {
// we're already disconnected, so just connect // we're already disconnected, so just connect
self.connect(force: true) connect(force: true)
} }
} }
func connect(force: Bool = false){ func connect(force: Bool = false) {
if !force && (self.isConnected || self.isConnecting) { if !force && (isConnected || isConnecting) {
return return
} }
var req = URLRequest(url: self.url)
req.timeoutInterval = 5
socket = make_websocket(url: url)
socket.delegate = self
isConnecting = true isConnecting = true
last_connection_attempt = Date().timeIntervalSince1970 last_connection_attempt = Date().timeIntervalSince1970
socket.connect() socket.connect()
@@ -68,7 +67,9 @@ class RelayConnection: WebSocketDelegate {
socket.write(string: req) socket.write(string: req)
} }
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) { func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event { switch event {
case .connected: case .connected:
@@ -83,15 +84,25 @@ class RelayConnection: WebSocketDelegate {
self.connect() self.connect()
} }
case .cancelled: fallthrough case .cancelled, .error:
case .error:
self.isConnecting = false self.isConnecting = false
self.isConnected = false self.isConnected = false
case .text(let txt): case .text(let txt):
if let ev = decode_nostr_event(txt: txt) { if txt.count > 2000 {
handleEvent(.nostr_event(ev)) DispatchQueue.global(qos: .default).async {
return 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)") print("decode failed for \(txt)")
@@ -103,7 +114,6 @@ class RelayConnection: WebSocketDelegate {
handleEvent(.ws_event(event)) handleEvent(.ws_event(event))
} }
} }
func make_nostr_req(_ req: NostrRequest) -> String? { func make_nostr_req(_ req: NostrRequest) -> String? {
@@ -127,7 +137,7 @@ func make_nostr_push_event(ev: NostrEvent) -> String? {
} }
func make_nostr_unsubscribe_req(_ sub_id: String) -> String? { 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? { func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? {
@@ -144,10 +154,3 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
req += "]" req += "]"
return req return req
} }
func make_websocket(url: URL) -> WebSocket {
let req = URLRequest(url: url)
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
return WebSocket(request: req, compressionHandler: .none)
}
+4 -18
View File
@@ -195,27 +195,13 @@ class RelayPool {
relay.connection.send(req) relay.connection.send(req)
} }
} }
func get_relays(_ ids: [String]) -> [Relay] { func get_relays(_ ids: [String]) -> [Relay] {
var relays: [Relay] = [] relays.filter { ids.contains($0.id) }
for id in ids {
if let relay = get_relay(id) {
relays.append(relay)
}
}
return relays
} }
func get_relay(_ id: String) -> Relay? { func get_relay(_ id: String) -> Relay? {
for relay in relays { relays.first(where: { $0.id == id })
if relay.id == id {
return relay
}
}
return nil
} }
func record_last_pong(relay_id: String, event: NostrConnectionEvent) { func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
+1
View File
@@ -143,6 +143,7 @@ func eightToFiveBits(_ input: [UInt8]) -> [UInt8] {
} }
/// Decode Bech32 string /// Decode Bech32 string
@discardableResult
public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? { public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
guard let strBytes = str.data(using: .utf8) else { guard let strBytes = str.data(using: .utf8) else {
throw Bech32Error.nonUTF8String throw Bech32Error.nonUTF8String
+27
View File
@@ -0,0 +1,27 @@
//
// Debouncer.swift
// damus
//
// Created by William Casarin on 2023-02-15.
//
import Foundation
class Debouncer {
private let queue = DispatchQueue.main
private var workItem: DispatchWorkItem?
private var interval: TimeInterval
init(interval: TimeInterval) {
self.interval = interval
}
func debounce(action: @escaping () -> Void) {
// Cancel the previous work item if it hasn't yet executed
workItem?.cancel()
// Create a new work item with a delay
workItem = DispatchWorkItem { action() }
queue.asyncAfter(deadline: .now() + interval, execute: workItem!)
}
}
+99
View File
@@ -0,0 +1,99 @@
//
// EventHolder.swift
// damus
//
// Created by William Casarin on 2023-02-19.
//
import Foundation
/// Used for holding back events until they're ready to be displayed
class EventHolder: ObservableObject {
private var has_event: Set<String>
@Published var events: [NostrEvent]
@Published var incoming: [NostrEvent]
@Published var should_queue: Bool
var queued: Int {
return incoming.count
}
var has_incoming: Bool {
return queued > 0
}
var all_events: [NostrEvent] {
events + incoming
}
init() {
self.should_queue = false
self.events = []
self.incoming = []
self.has_event = Set()
}
init(events: [NostrEvent], incoming: [NostrEvent]) {
self.should_queue = false
self.events = events
self.incoming = incoming
self.has_event = Set()
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
self.events = self.events.filter(isIncluded)
self.incoming = self.incoming.filter(isIncluded)
}
func insert(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_queued(ev)
} else {
return insert_immediate(ev)
}
}
private func insert_immediate(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) {
return true
}
return false
}
private func insert_queued(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
incoming.append(ev)
return true
}
func flush() {
guard !incoming.isEmpty else {
return
}
var changed = false
for event in incoming {
if insert_uniq_sorted_event_created(events: &events, new_ev: event) {
changed = true
}
}
if changed {
self.objectWillChange.send()
}
self.incoming = []
}
}
+9
View File
@@ -38,6 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true return true
} }
@discardableResult
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
var i: Int = 0 var i: Int = 0
@@ -58,6 +59,14 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return true return true
} }
func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event(events: &events, new_ev: new_ev) {
$0.created_at > $1.created_at
}
}
@discardableResult
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool { func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0 var i: Int = 0
+148
View File
@@ -0,0 +1,148 @@
//
// KFOptionSetter+.swift
// damus
//
// Created by Oleg Abalonski on 2/15/23.
//
import UIKit
import Kingfisher
extension KFOptionSetter {
func imageContext(_ imageContext: ImageContext) -> Self {
options.callbackQueue = .dispatch(.global(qos: .background))
options.processingQueue = .dispatch(.global(qos: .background))
options.downloader = CustomImageDownloader.shared
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.processor = CustomImageProcessor(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.cacheSerializer = CustomCacheSerializer(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
return self
}
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]
return self
}
}
let MAX_FILE_SIZE = 20_971_520 // 20MiB
enum ImageContext {
case pfp
case banner
case note
func maxMebibyteSize() -> Int {
switch self {
case .pfp:
return 5_242_880 // 5Mib
case .banner, .note:
return 20_971_520 // 20MiB
}
}
func downsampleSize() -> CGSize {
switch self {
case .pfp:
return CGSize(width: 200, height: 200)
case .banner:
return CGSize(width: 750, height: 250)
case .note:
return CGSize(width: 500, height: 500)
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let contentLength = response.expectedContentLength
// Content-Length header is optional (-1 when missing)
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
}
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
}
}
class CustomImageDownloader: ImageDownloader {
static let shared = CustomImageDownloader(name: "shared")
override init(name: String) {
super.init(name: name)
sessionDelegate = CustomSessionDelegate()
}
}
+5 -2
View File
@@ -8,8 +8,8 @@
import Foundation import Foundation
public struct Markdown { 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. /// Ensure the specified URL has a scheme by prepending "https://" if it's absent.
static func withScheme(_ url: any StringProtocol) -> any StringProtocol { static func withScheme(_ url: any StringProtocol) -> any StringProtocol {
return url.contains("://") ? url : "https://" + url return url.contains("://") ? url : "https://" + url
@@ -31,6 +31,9 @@ public struct Markdown {
/// Process the input text and add markdown for any embedded URLs. /// Process the input text and add markdown for any embedded URLs.
public func process(_ input: String) -> AttributedString { 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)) let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
var output = input var output = input
// Start with the last match, because replacing the first would invalidate all subsequent indices // Start with the last match, because replacing the first would invalidate all subsequent indices
+3
View File
@@ -98,6 +98,9 @@ extension Notification.Name {
static var deleted_account: Notification.Name { static var deleted_account: Notification.Name {
return Notification.Name("deleted_account") return Notification.Name("deleted_account")
} }
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
} }
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+8
View File
@@ -25,4 +25,12 @@ class Theme {
UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black
} }
static var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
} }
+2 -1
View File
@@ -285,12 +285,13 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint return endpoint
} }
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, amount: Int64) async -> String? { func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil return nil
} }
let zappable = payreq.allowsNostr ?? false let zappable = payreq.allowsNostr ?? false
let amount: Int64 = Int64(sats) * 1000
var query = [URLQueryItem(name: "amount", value: "\(amount)")] var query = [URLQueryItem(name: "amount", value: "\(amount)")]
+2 -2
View File
@@ -36,7 +36,7 @@ class Zaps {
if our_zaps[note_target.note_id] == nil { if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap] our_zaps[note_target.note_id] = [zap]
} else { } else {
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap) insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
} }
case .profile(_): case .profile(_):
break break
@@ -60,6 +60,6 @@ class Zaps {
event_counts[id] = event_counts[id]! + 1 event_counts[id] = event_counts[id]! + 1
event_totals[id] = event_totals[id]! + zap.invoice.amount event_totals[id] = event_totals[id]! + zap.invoice.amount
return notify(.update_stats, zap.target.id)
} }
} }
+21 -11
View File
@@ -28,13 +28,14 @@ struct EventActionBar: View {
@State var sheet: ActionBarSheet? = nil @State var sheet: ActionBarSheet? = nil
@State var confirm_boost: Bool = false @State var confirm_boost: Bool = false
@State var show_share_sheet: Bool = false @State var show_share_sheet: Bool = false
@StateObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel, test_lnurl: String? = nil) { @ObservedObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
self.damus_state = damus_state self.damus_state = damus_state
self.event = event self.event = event
self.test_lnurl = test_lnurl self.test_lnurl = test_lnurl
_bar = StateObject.init(wrappedValue: bar) _bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
} }
var lnurl: String? { var lnurl: String? {
@@ -50,7 +51,7 @@ struct EventActionBar: View {
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button")) .accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
} }
Spacer() Spacer()
ZStack { HStack(spacing: 4) {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) { EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted { if bar.boosted {
@@ -60,14 +61,13 @@ struct EventActionBar: View {
} }
} }
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button")) .accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")") Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 18)
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray) .foregroundColor(bar.boosted ? Color.green : Color.gray)
} }
Spacer() Spacer()
ZStack { HStack(spacing: 4) {
LikeButton(liked: bar.liked) { LikeButton(liked: bar.liked) {
if bar.liked { if bar.liked {
notify(.delete, bar.our_like) notify(.delete, bar.our_like)
@@ -75,8 +75,7 @@ struct EventActionBar: View {
send_like() send_like()
} }
} }
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")") Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.offset(x: 22)
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.foregroundColor(bar.liked ? Color.accentColor : Color.gray) .foregroundColor(bar.liked ? Color.accentColor : Color.gray)
@@ -110,6 +109,11 @@ struct EventActionBar: View {
} message: { } message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.") Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
} }
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
self.bar.update(damus: self.damus_state, evid: target)
}
.onReceive(handle_notify(.liked)) { n in .onReceive(handle_notify(.liked)) { n in
let liked = n.object as! Counted let liked = n.object as! Counted
if liked.id != event.id { if liked.id != event.id {
@@ -152,9 +156,9 @@ struct EventActionBar: View {
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View { func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
Button(action: action) { Button(action: action) {
Label(NSLocalizedString("\u{00A0}", comment: "Non-breaking space character to fill in blank space next to event action button icons."), systemImage: img) Image(systemName: img)
.font(.footnote.weight(.medium))
.foregroundColor(col == nil ? Color.gray : col!) .foregroundColor(col == nil ? Color.gray : col!)
.font(.footnote.weight(.medium))
} }
} }
@@ -184,6 +188,8 @@ struct EventActionBar_Previews: PreviewProvider {
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil) let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil) let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil) let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil) let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) { VStack(spacing: 50) {
@@ -194,7 +200,11 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours) EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar) EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl") EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
} }
.padding(20) .padding(20)
+13 -4
View File
@@ -11,20 +11,29 @@ struct EventDetailBar: View {
let state: DamusState let state: DamusState
let target: String let target: String
let target_pk: String let target_pk: String
@ObservedObject var bar: ActionBarModel @ObservedObject var bar: ActionBarModel
init (state: DamusState, target: String, target_pk: String) {
self.state = state
self.target = target
self.target_pk = target_pk
self._bar = ObservedObject(wrappedValue: make_actionbar_model(ev: target, damus: state))
}
var body: some View { var body: some View {
HStack { HStack {
if bar.boosts > 0 { if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) { NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text("\(bar.boosts)", comment: "Number of reposts.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reposts_count", comment: "Part of a larger sentence to describe how many reposts there are."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
if bar.likes > 0 { if bar.likes > 0 {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) { NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
Text("\(Text("\(bar.likes)", comment: "Number of reactions on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reactions_count", comment: "Part of a larger sentence to describe how many reactions there are on a post."), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
@@ -32,7 +41,7 @@ struct EventDetailBar: View {
if bar.zaps > 0 { if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk)) let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) { NavigationLink(destination: dst) {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.") Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
@@ -42,6 +51,6 @@ struct EventDetailBar: View {
struct EventDetailBar_Previews: PreviewProvider { struct EventDetailBar_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
EventDetailBar(state: test_damus_state(), target: "", target_pk: "", bar: ActionBarModel.empty()) EventDetailBar(state: test_damus_state(), target: "", target_pk: "")
} }
} }
+5 -22
View File
@@ -10,40 +10,23 @@ import Kingfisher
struct InnerBannerImageView: View { struct InnerBannerImageView: View {
let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage() let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@ObservedObject var imageModel: KFImageModel
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 20_971_520, // 20 MiB
downsampleSize: CGSize(width: 750, height: 250)
)
}
var body: some View { var body: some View {
ZStack { ZStack {
Color(uiColor: .systemBackground) Color(uiColor: .systemBackground)
if (imageModel.url != nil) { if (url != nil) {
KFAnimatedImage(imageModel.url) KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background))) .imageContext(.banner)
.processingQueue(.dispatch(.global(qos: .background)))
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
.configure { view in .configure { view in
view.framePreloadCount = 1 view.framePreloadCount = 3
} }
.placeholder { _ in .placeholder { _ in
Color(uiColor: .secondarySystemBackground) Color(uiColor: .secondarySystemBackground)
} }
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailureImage(defaultImage) .onFailureImage(defaultImage)
.id(imageModel.refreshID)
} else { } else {
Image(uiImage: defaultImage).resizable() Image(uiImage: defaultImage).resizable()
} }
+6 -6
View File
@@ -63,7 +63,7 @@ struct ChatView: View {
} }
var ReplyDescription: some View { var ReplyDescription: some View {
Text("\(reply_desc(profiles: damus_state.profiles, event: event))") Text(verbatim: "\(reply_desc(profiles: damus_state.profiles, event: event))")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.frame(alignment: .leading) .frame(alignment: .leading)
@@ -89,12 +89,12 @@ struct ChatView: View {
ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), damus: damus_state, show_friend_confirmed: true) ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), damus: damus_state, show_friend_confirmed: true)
.foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black) .foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black)
//.shadow(color: Color.black, radius: 2) //.shadow(color: Color.black, radius: 2)
Text("\(format_relative_time(event.created_at))") Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
if let ref_id = thread.replies.lookup(event.id) { if let _ = thread.replies.lookup(event.id) {
if !is_reply_to_prev() { if !is_reply_to_prev() {
/* /*
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews) ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
@@ -112,11 +112,11 @@ struct ChatView: View {
NoteContentView(damus_state: damus_state, NoteContentView(damus_state: damus_state,
event: event, event: event,
show_images: show_images, show_images: show_images,
artifacts: .just_content(event.content), size: .normal,
size: .normal) artifacts: .just_content(event.content))
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event, damus: damus_state) let bar = make_actionbar_model(ev: event.id, damus: damus_state)
EventActionBar(damus_state: damus_state, event: event, bar: bar) EventActionBar(damus_state: damus_state, event: event, bar: bar)
} }
+33
View File
@@ -8,6 +8,7 @@ import AVFoundation
import Kingfisher import Kingfisher
import SwiftUI import SwiftUI
import LocalAuthentication import LocalAuthentication
import Combine
struct ConfigView: View { struct ConfigView: View {
let state: DamusState let state: DamusState
@@ -21,6 +22,7 @@ struct ConfigView: View {
@State var privkey_copied: Bool = false @State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false @State var pubkey_copied: Bool = false
@State var delete_text: String = "" @State var delete_text: String = ""
@State var default_zap_amount: String
@ObservedObject var settings: UserSettingsStore @ObservedObject var settings: UserSettingsStore
@@ -28,6 +30,8 @@ struct ConfigView: View {
init(state: DamusState) { init(state: DamusState) {
self.state = state self.state = state
let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000"
_default_zap_amount = State(initialValue: zap_amt)
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "") _privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_settings = ObservedObject(initialValue: state.settings) _settings = ObservedObject(initialValue: state.settings)
} }
@@ -124,6 +128,29 @@ struct ConfigView: View {
} }
} }
} }
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
let filtered = newValue.filter { Set("0123456789").contains($0) }
if filtered != newValue {
default_zap_amount = filtered
}
if filtered == "" {
set_default_zap_amount(pubkey: state.pubkey, amount: 1000)
return
}
guard let amt = Int(filtered) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) { Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) { Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
@@ -192,6 +219,12 @@ struct ConfigView: View {
} }
} }
} }
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
}
} }
} }
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view.")) .navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
+2 -2
View File
@@ -36,14 +36,14 @@ struct CreateAccountView: View {
HStack(alignment: .top) { HStack(alignment: .top) {
VStack { VStack {
Text(" ", comment: "Blank space to separate profile picture from profile editor form.") Text(verbatim: " ")
.foregroundColor(.white) .foregroundColor(.white)
} }
VStack { VStack {
SignupForm { SignupForm {
FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry.")) FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry."))
HStack(spacing: 0.0) { HStack(spacing: 0.0) {
Text("@", comment: "Prefix character to username.") Text(verbatim: "@")
.foregroundColor(.white) .foregroundColor(.white)
.padding(.leading, -25.0) .padding(.leading, -25.0)
+11 -7
View File
@@ -11,7 +11,6 @@ struct DMChatView: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String let pubkey: String
@EnvironmentObject var dms: DirectMessageModel @EnvironmentObject var dms: DirectMessageModel
@State var message: String = ""
@State var showPrivateKeyWarning: Bool = false @State var showPrivateKeyWarning: Bool = false
var Messages: some View { var Messages: some View {
@@ -52,7 +51,7 @@ struct DMChatView: View {
} }
var InputField: some View { var InputField: some View {
TextEditor(text: $message) TextEditor(text: $dms.draft)
.textEditorBackground { .textEditorBackground {
InputBackground() InputBackground()
} }
@@ -93,11 +92,11 @@ struct DMChatView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
InputField InputField
if !message.isEmpty { if !dms.draft.isEmpty {
Button( Button(
role: .none, role: .none,
action: { action: {
showPrivateKeyWarning = contentContainsPrivateKey(message) showPrivateKeyWarning = contentContainsPrivateKey(dms.draft)
if !showPrivateKeyWarning { if !showPrivateKeyWarning {
send_message() send_message()
@@ -112,7 +111,7 @@ struct DMChatView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom) .frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
Text(message).opacity(0).padding(.all, 8) Text(dms.draft).opacity(0).padding(.all, 8)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom) .frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
} }
@@ -122,7 +121,7 @@ struct DMChatView: View {
func send_message() { func send_message() {
let tags = [["p", pubkey]] let tags = [["p", pubkey]]
let post_blocks = parse_post_blocks(content: message) let post_blocks = parse_post_blocks(content: dms.draft)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags) let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let content = render_blocks(blocks: post_tags.blocks) let content = render_blocks(blocks: post_tags.blocks)
@@ -131,7 +130,7 @@ struct DMChatView: View {
return return
} }
message = "" dms.draft = ""
damus_state.pool.send(.event(dm)) damus_state.pool.send(.event(dm))
end_editing() end_editing()
@@ -157,6 +156,11 @@ struct DMChatView: View {
} }
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.")) .navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
.toolbar { Header } .toolbar { Header }
.onDisappear {
if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
dms.draft = ""
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: { .alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) { Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false showPrivateKeyWarning = false
+1 -1
View File
@@ -23,7 +23,7 @@ struct DMView: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal) NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)))
.foregroundColor(is_ours ? Color.white : Color.primary) .foregroundColor(is_ours ? Color.white : Color.primary)
.padding(10) .padding(10)
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) .background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
+2 -1
View File
@@ -33,13 +33,14 @@ struct DirectMessagesView: View {
NavigationLink(destination: chat, isActive: $open_dm) { NavigationLink(destination: chat, isActive: $open_dm) {
EmptyView() EmptyView()
} }
LazyVStack { LazyVStack(spacing: 0) {
if model.dms.isEmpty, !model.loading { if model.dms.isEmpty, !model.loading {
EmptyTimelineView() EmptyTimelineView()
} else { } else {
let dms = requests ? model.message_requests : model.friend_dms let dms = requests ? model.message_requests : model.friend_dms
ForEach(dms, id: \.0) { tup in ForEach(dms, id: \.0) { tup in
MaybeEvent(tup) MaybeEvent(tup)
.padding(.top, 10)
} }
} }
} }
+4 -4
View File
@@ -10,7 +10,7 @@ import SwiftUI
struct EventDetailView: View { struct EventDetailView: View {
var body: some View { var body: some View {
Text("EventDetailView") Text(verbatim: "EventDetailView")
} }
} }
@@ -33,14 +33,14 @@ func print_event(_ ev: NostrEvent) {
print(ev.description) print(ev.description)
} }
func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool) { func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, animate: Bool, anchor: UnitPoint = .bottom) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if animate { if animate {
withAnimation { withAnimation {
scroller.scrollTo(id, anchor: .bottom) scroller.scrollTo(id, anchor: anchor)
} }
} else { } else {
scroller.scrollTo(id, anchor: .bottom) scroller.scrollTo(id, anchor: anchor)
} }
} }
} }
+8 -10
View File
@@ -152,16 +152,14 @@ func format_date(_ created_at: Int64) -> String {
return dateFormatter.string(from: date) return dateFormatter.string(from: date)
} }
func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
let likes = damus.likes.counts[ev]
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel { let boosts = damus.boosts.counts[ev]
let likes = damus.likes.counts[ev.id] let zaps = damus.zaps.event_counts[ev]
let boosts = damus.boosts.counts[ev.id] let zap_total = damus.zaps.event_totals[ev]
let zaps = damus.zaps.event_counts[ev.id] let our_like = damus.likes.our_events[ev]
let zap_total = damus.zaps.event_totals[ev.id] let our_boost = damus.boosts.our_events[ev]
let our_like = damus.likes.our_events[ev.id] let our_zap = damus.zaps.our_zaps[ev]
let our_boost = damus.boosts.our_events[ev.id]
let our_zap = damus.zaps.our_zaps[ev.id]
return ActionBarModel(likes: likes ?? 0, return ActionBarModel(likes: likes ?? 0,
boosts: boosts ?? 0, boosts: boosts ?? 0,
+1 -2
View File
@@ -53,8 +53,7 @@ struct BuilderEventView: View {
NostrFilter(ids: [self.event_id], limit: 1), NostrFilter(ids: [self.event_id], limit: 1),
NostrFilter( NostrFilter(
kinds: [NostrKind.zap.rawValue], kinds: [NostrKind.zap.rawValue],
referenced_ids: [self.event_id], referenced_ids: [self.event_id]
limit: 500
) )
]) ])
} }
+1 -1
View File
@@ -23,7 +23,7 @@ struct EventBody: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil) let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(content), size: size) NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
} }
+13 -9
View File
@@ -13,7 +13,7 @@ struct ReplyDescription: View {
let profiles: Profiles let profiles: Profiles
var body: some View { var body: some View {
Text("\(reply_desc(profiles: profiles, event: event))") Text(verbatim: "\(reply_desc(profiles: profiles, event: event))")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -32,7 +32,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
let n = desc.others let n = desc.others
if desc.pubkeys.count == 0 { if desc.pubkeys.count == 0 {
return NSLocalizedString("Reply to self", comment: "Label to indicate that the user is replying to themself.") return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.")
} }
let names: [String] = pubkeys.map { let names: [String] = pubkeys.map {
@@ -40,16 +40,20 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
return Profile.displayName(profile: prof, pubkey: $0) return Profile.displayName(profile: prof, pubkey: $0)
} }
if names.count == 2 { let othersCount = n - pubkeys.count
if n > 2 { if names.count > 1 {
let othersCount = n - pubkeys.count if othersCount == 0 {
return String(format: NSLocalizedString("replying_to_two_and_others", comment: "Label to indicate that the user is replying to 2 users and others."), names[0], names[1], othersCount) return String(format: "Replying to %@ & %@", names[0], names[1])
} else {
return String(format: "Replying to %@, %@ & %d others", names[0], names[1], othersCount)
} }
return String(format: NSLocalizedString("Replying to %@ & %@", comment: "Label to indicate that the user is replying to 2 users."), names[0], names[1])
} }
let othersCount = n - pubkeys.count if othersCount == 0 {
return String(format: NSLocalizedString("replying_to_one_and_others", comment: "Label to indicate that the user is replying to 1 user and others."), names[0], othersCount) return String(format: "Replying to %@", names[0])
} else {
return String(format: "Replying to %@ & %d others", names[0], othersCount)
}
} }
+16 -5
View File
@@ -15,6 +15,14 @@ struct SelectedEventView: View {
event.pubkey event.pubkey
} }
@StateObject var bar: ActionBarModel
init(damus: DamusState, event: NostrEvent) {
self.damus = damus
self.event = event
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
}
var body: some View { var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey) let profile = damus.profiles.lookup(id: pubkey)
@@ -27,7 +35,7 @@ struct SelectedEventView: View {
BuilderEventView(damus: damus, event_id: mention.ref.id) BuilderEventView(damus: damus, event_id: mention.ref.id)
} }
Text("\(format_date(event.created_at))") Text(verbatim: "\(format_date(event.created_at))")
.padding(.top, 10) .padding(.top, 10)
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
@@ -35,19 +43,22 @@ struct SelectedEventView: View {
Divider() Divider()
.padding([.bottom], 4) .padding([.bottom], 4)
let bar = make_actionbar_model(ev: event, damus: damus)
if !bar.is_empty { if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey, bar: bar) EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
Divider() Divider()
} }
EventActionBar(damus_state: damus, event: event, bar: bar) EventActionBar(damus_state: damus, event: event)
.padding([.top], 4) .padding([.top], 4)
Divider() Divider()
.padding([.top], 4) .padding([.top], 4)
} }
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target)
}
.padding([.leading], 2) .padding([.leading], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey) .event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
} }
+2 -4
View File
@@ -33,7 +33,7 @@ struct TextEvent: View {
HStack(alignment: .center) { HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal) EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text("\(format_relative_time(event.created_at))") Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray) .foregroundColor(.gray)
Spacer() Spacer()
@@ -48,9 +48,7 @@ struct TextEvent: View {
if has_action_bar { if has_action_bar {
Rectangle().frame(height: 2).opacity(0) Rectangle().frame(height: 2).opacity(0)
let bar = make_actionbar_model(ev: event, damus: damus) EventActionBar(damus_state: damus, event: event)
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4) .padding([.top], 4)
} }
} }
+5 -5
View File
@@ -70,19 +70,19 @@ struct FollowButtonPreviews: View {
let target: FollowTarget = .pubkey("") let target: FollowTarget = .pubkey("")
var body: some View { var body: some View {
VStack { VStack {
Text("Unfollows", comment: "Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.") Text(verbatim: "Unfollows")
FollowButtonView(target: target, follows_you: false, follow_state: .unfollows) FollowButtonView(target: target, follows_you: false, follow_state: .unfollows)
Text("Following", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.") Text(verbatim: "Following")
FollowButtonView(target: target, follows_you: false, follow_state: .following) FollowButtonView(target: target, follows_you: false, follow_state: .following)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.") Text(verbatim: "Follows")
FollowButtonView(target: target, follows_you: false, follow_state: .follows) FollowButtonView(target: target, follows_you: false, follow_state: .follows)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.") Text(verbatim: "Follows")
FollowButtonView(target: target, follows_you: true, follow_state: .follows) FollowButtonView(target: target, follows_you: true, follow_state: .follows)
Text("Unfollowing", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.") Text(verbatim: "Unfollowing")
FollowButtonView(target: target, follows_you: false, follow_state: .unfollowing) FollowButtonView(target: target, follows_you: false, follow_state: .unfollowing)
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ struct MentionView: View {
let pk = bech32_pubkey(mention.ref.ref_id) ?? mention.ref.ref_id let pk = bech32_pubkey(mention.ref.ref_id) ?? mention.ref.ref_id
PubkeyView(pubkey: pk, relay: mention.ref.relay_id) PubkeyView(pubkey: pk, relay: mention.ref.relay_id)
case .event: case .event:
Text("< e >", comment: "Placeholder for event mention.") Text(verbatim: "< e >")
//EventBlockView(pubkey: mention.ref.ref_id, relay: mention.ref.relay_id) //EventBlockView(pubkey: mention.ref.ref_id, relay: mention.ref.relay_id)
} }
} }
+28 -16
View File
@@ -9,37 +9,49 @@ import SwiftUI
import LinkPresentation import LinkPresentation
import NaturalLanguage import NaturalLanguage
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemUltraThinMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
struct NoteContentView: View { struct NoteContentView: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let event: NostrEvent
let show_images: Bool let show_images: Bool
let size: EventViewKind
@State var artifacts: NoteArtifacts @State var artifacts: NoteArtifacts
let size: EventViewKind
@State var preview: LinkViewRepresentable? = nil @State var preview: LinkViewRepresentable? = nil
func MainContent() -> some View { func MainContent() -> some View {
return VStack(alignment: .leading) { return VStack(alignment: .leading) {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
if size == .selected { if size == .selected {
TranslateView(damus_state: damus_state, event: event, size: size) SelectableText(attributedString: artifacts.content)
TranslateView(damus_state: damus_state, event: event)
} else {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
} }
if show_images && artifacts.images.count > 0 { if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images) ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 { } else if !show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images) ZStack {
.blur(radius: 10) ImageCarousel(urls: artifacts.images)
.overlay { Blur()
Rectangle() .disabled(true)
.opacity(0.50) }
} .cornerRadius(10)
.cornerRadius(10)
} }
if artifacts.invoices.count > 0 { if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices) InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
@@ -155,7 +167,7 @@ struct NoteContentView_Previews: PreviewProvider {
let state = test_damus_state() let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg" let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: []) let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, artifacts: artifacts, size: .normal) NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts)
} }
} }

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