Compare commits

..

82 Commits

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

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

Changelog-Fixed: Fix lag on startup when you have lots of DMs
Changelog-Fixed: Fix an issues where dm notifications appear without any new events
2023-02-15 11:14:13 -08:00
William Casarin cf90480501 perf: decode large events in background threads
should help with hitches a bit
2023-02-15 11:14:13 -08:00
OlegAba f0075904c2 Fix frequent KFImage hang
Changelog-Fixed: Fix some hangs when scrolling by images
Closes: #614
2023-02-15 11:13:04 -08:00
tyiu a41acc12e7 Import translations 2023-02-15 12:50:57 -05:00
William Casarin 1e22984d52 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 08:49:45 -08:00
tyiu 9080e4efae Force default zap amount text field to accept only numbers
Changelog-Fixed: Force default zap amount text field to accept only numbers
Closes: #612
2023-02-15 08:47:22 -08:00
tyiu 6488634eda Import translations 2023-02-15 10:54:45 -05:00
tyiu 355cd1283c Wrap non-translatable strings so that they do not get exported 2023-02-15 10:44:44 -05:00
William Casarin 6ed9c408f9 v1.1.0-2 changelog 2023-02-14 10:17:08 -08:00
William Casarin 5f52e6f62f v1.1.0-2 2023-02-14 10:15:40 -08:00
William Casarin 59211bb4fd Ensure stats get updated in realtime on action bars
Changelog-Fixed: Ensure stats get updated in realtime on action bars
2023-02-14 10:05:59 -08:00
William Casarin 6d634763c5 Fix repost counters
Changelog-Fixed: Fix reposts not getting counted properly
2023-02-14 10:05:19 -08:00
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
tyiu 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 Goet 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 Goet bb9fc6f905 Always stop at first whitespace 2023-02-13 10:03:17 -08:00
tyiu 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 Gordiichuk 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
tyiu 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 Montz 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 Weeks 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
### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
@@ -13,7 +65,6 @@
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
- Add accessibility labels to action bar (Bryan Montz)
- Copy invoice button (Joel Klabo)
- Ability to change remote image loading policy (radixrat)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
@@ -23,10 +74,8 @@
- Show "Follow Back" button on profile page (William Casarin)
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
- Updated QR code view, include profile image, etc (ericholguin)
- Make app smaller by optimizing pngs (pea-sys)
- Clicking relay numbers now goes to relay config (radixrat)
### Fixed
- 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
-18
View File
@@ -108,24 +108,6 @@ All user-facing strings must have a comment in order to provide context to trans
[transifex]: https://explore.transifex.com/damus/damus-ios/
#### Export Source Translations
If user-facing strings have been added or changed, please export them for translation as part of your pull request or commit by running:
```zsh
./devtools/export-source-translation.sh
```
This command will export source translations to `translations/en-US.xcloc/Localized Contents/en-US.xliff`, which the Transifex integration will read from the `master` branch and allow translators to translate those strings.
#### Import Translations
Once 100% of strings have been translated for a given locale, Transifex will open up a pull request with the `translations/<locale>.xliff` file changed. Currently, it must be manually imported into the project before merging the pull request by running:
```zsh
./devtools/import-translation.sh <locale_code_in_snake_case>
```
### Awards
There may be nostr badges awarded for contributors in the future... :)
+9 -2
View File
@@ -26,6 +26,10 @@ static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
@@ -55,8 +59,8 @@ static int consume_until_whitespace(struct cursor *cur, int or_end) {
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c) && consumedAtLeastOne)
return 1;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = true;
@@ -221,6 +225,9 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 0;
}
// strip any unwanted characters
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
block->type = BLOCK_URL;
block->block.str.start = (const char *)start;
block->block.str.end = (const char *)cur->p;
+69 -12
View File
@@ -15,6 +15,7 @@
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
@@ -41,6 +42,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; };
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -154,6 +156,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -197,6 +202,7 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
@@ -204,9 +210,10 @@
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
@@ -216,7 +223,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -248,6 +255,9 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -258,6 +268,12 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -270,6 +286,7 @@
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -310,6 +327,7 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; };
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -453,6 +471,9 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -499,15 +520,17 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
@@ -517,7 +540,7 @@
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -653,13 +676,13 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -667,6 +690,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
@@ -705,7 +729,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -777,6 +801,9 @@
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -839,6 +866,15 @@
path = Events;
sourceTree = "<group>";
};
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup;
children = (
@@ -857,6 +893,7 @@
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -916,6 +953,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
@@ -1113,6 +1151,9 @@
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1204,9 +1245,11 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1243,7 +1286,6 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1251,6 +1293,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
@@ -1274,6 +1317,7 @@
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
@@ -1324,6 +1368,7 @@
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -1339,6 +1384,7 @@
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
@@ -1352,6 +1398,7 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
);
@@ -1370,6 +1417,7 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
@@ -1417,6 +1465,9 @@
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1437,6 +1488,9 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1457,6 +1511,9 @@
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1592,7 +1649,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1615,7 +1672,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1634,7 +1691,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1657,7 +1714,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
"version" : "7.4.0"
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.6.1"
}
},
{
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-copy.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

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

Before

Width:  |  Height:  |  Size: 400 B

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

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

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

Before

Width:  |  Height:  |  Size: 950 B

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

Before

Width:  |  Height:  |  Size: 252 B

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

Before

Width:  |  Height:  |  Size: 458 B

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

Before

Width:  |  Height:  |  Size: 671 B

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

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