Compare commits

...

141 Commits

Author SHA1 Message Date
b8d9e34b84 Fix localization issues, add tests, import translations, and add zh-CN and zh-TW 2023-02-24 12:26:43 -05:00
Joel Klabo
8b9958a4ad Add Bookmarking (Local to device)
Changelog-Added: Bookmarking
Closes: #649
2023-02-21 12:33:21 -08:00
William Casarin
87a0bdac94 Load missing profiles in Zaps view
Changelog-Fixed: Load missing profiles in Zaps view
2023-02-21 10:08:15 -08:00
William Casarin
37b964c296 Fix issue where CPU is continuously pegged when scrolling
Since should_queue is continuosly getting set, this is causing the
InnerTimelineView to continuously rerender, pegging the CPU to 100%

This change only updates the var if it changes from the previous value.
2023-02-21 05:07:27 -08:00
William Casarin
b1a2b47116 Eliminate popping when scrolling
This commit makes a few changes:

- Link preview views are no longer cached, only the metadata. This fixes
  a memory leak when preview videos. It will keep playing the video
  forever eventually leading to a crash. This is fixed!

- Cache the intrinsic height of previews, when loading notes it looks
  for the cached height so that things don't pop-in after the fact

- Note artifacts and previews are set in the constructor instead of
  onAppear, this prevents the size from changing and popping after it
  has been loaded into the lazyvstack

Changelog-Fixed: Fix memory leak with inline videos
Changelog-Fixed: Eliminate popping when scrolling
2023-02-21 04:36:01 -08: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
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
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
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
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
36f05ccaed Export and import translations 2023-02-16 10:17:09 -05:00
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
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
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
6488634eda Import translations 2023-02-15 10:54:45 -05:00
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
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
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
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
59ccde9c38 Import translations 2023-02-13 11:43:06 -05:00
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
William Casarin
679c0ac424 v1.0.0-15 changelog 2023-02-10 12:55:36 -08:00
William Casarin
48050f5e69 build 15 2023-02-10 12:54:59 -08:00
William Casarin
b5c967e161 Use cached zap if we have it 2023-02-10 12:48:28 -08:00
William Casarin
715d4aa35d List zaps on posts 2023-02-10 12:43:26 -08:00
William Casarin
4b54278378 dismiss relay config on timeline change 2023-02-10 11:28:30 -08:00
William Casarin
6e700e5726 Rename delete account to "permanently delete account" 2023-02-10 11:27:44 -08:00
William Casarin
18ad113198 ActionBarModel: StateObject -> ObservedObject 2023-02-10 11:27:12 -08:00
William Casarin
7415671900 Load zaps, likes and reposts when you open a thread
Changelog-Fixed: Load zaps, likes and reposts when you open a thread
2023-02-10 11:09:13 -08:00
William Casarin
d5b6d935e8 fix tests 2023-02-10 11:08:28 -08:00
543fd67f35 Import japanese translations
Changelog-Added: Japanese translations
2023-02-10 10:52:28 -08:00
Brian Lee
c24689d3ef Public relay name change
Closes: #555
2023-02-10 10:24:49 -08:00
bf7120dc08 Add password autofill on account login and creation
Changelog-Added: Add password autofill on account login and creation
Closes: #559
2023-02-10 10:14:44 -08:00
William Casarin
dbe938ad9b Add paid relay details 2023-02-10 10:01:17 -08:00
William Casarin
289d55b918 Show paid or global relay type next to relay status
Changelog-Added: Show if relay is paid
2023-02-10 09:37:26 -08:00
William Casarin
771fa845e3 Fix bug where sidebar navigation fails to pop when switching timelines
Changelog-Fixed: Fix bug where sidebar navigation fails to pop when switching timelines
2023-02-10 09:37:26 -08:00
William Casarin
5f1545b86a Paid relay detection 2023-02-10 08:27:54 -08:00
OlegAba
9a95967a81 Refactor pfp image view to use zoomable scroll view 2023-02-10 01:03:55 -05:00
William Casarin
fe444228e6 Cached relay metadata 2023-02-09 15:56:26 -08: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
William Casarin
10596ddb09 Relay Filters
wip
2023-02-09 14:23:18 -08:00
William Casarin
989684cd37 Use lnaddress before lnurl for tip addresses to avoid Anigma scamming
Since anigma is scamming and setting people's lnurls

Changelog-Fixed: Use lnaddress before lnurl for tip addresses to avoid Anigma scamming
2023-02-08 12:56:19 -08:00
OlegAba
9ab03034a2 Refactor side menu
Changelog-Fixed: Fix sidebar navigation bugs
Closes: #460
2023-02-08 09:56:16 -08:00
c602c754f8 Import translations 2023-02-08 11:47:46 -05:00
c5db887ab1 Export source translations 2023-02-07 23:23:51 -05:00
William Casarin
0563ec8bf8 Fix issue where navigation fails pop to root when switching timelines
Sometimes the navigation stack fails to pop, fix this

Changelog-Fixed: Fix issue where navigation fails pop to root when switching timelines
2023-02-07 14:09:07 -08:00
William Casarin
29c4170833 Make @ mentions case insensitive
Changelog-Fixed: Make @ mentions case insensitive
2023-02-07 12:07:59 -08:00
William Casarin
3c629621eb Add "Follows You" to profile
Changelog-Added: Add "Follows You" indicator on profile
2023-02-07 10:51:08 -08:00
William Casarin
09f12845c0 FollowButton: show "Follows You" if they follow you
Changelog-Changed: Show "Follow Back" button on profile page
2023-02-07 10:22:19 -08:00
William Casarin
b882a96206 profile/refactor: break name section into its own function 2023-02-07 10:22:19 -08:00
William Casarin
fb82cc0531 ProfileModel: add follows helper
This will be used for "Follows You" logic
2023-02-07 10:20:12 -08:00
William Casarin
90cd48ead7 Merge remote-tracking branch 'tyiu/tyiu/export-translations' 2023-02-07 09:57:17 -08:00
William Casarin
f71b67f036 relays: refactor 2023-02-07 09:56:46 -08:00
4406e44424 Export translations 2023-02-07 00:02:26 -05:00
William Casarin
ae6608cf7d Revert "Add screen to select individual relays when posting/broadcasting"
This reverts commit 04759107a2.
2023-02-06 14:40:54 -08:00
Andrii Sievrikov
04759107a2 Add screen to select individual relays when posting/broadcasting
Changelog-Added: Add screen to select individual relays when posting/broadcasting
Closes: #525
2023-02-06 13:50:52 -08:00
Joel Klabo
552402f2b5 Add Relay Detail View
Changelog-Added: Relay Detail View
Closes: #479
2023-02-06 13:21:42 -08:00
852609ee30 Add alert to warn against posting nsec1 private keys
Changelog-Added: Warn when attempting to post an nsec key
Closes: #498
2023-02-06 11:59:44 -08:00
William Casarin
1e44d97a97 refactor: pk_settings_key
will use this in the future I'm sure
2023-02-06 11:55:20 -08:00
567303e680 Add DeepL translation integration
Changelog-Added: DeepL translation integration
Closes: #522
2023-02-06 11:51:50 -08:00
7d1bac4028 Import translations
Closes: #523
2023-02-06 11:31:40 -08:00
William Casarin
eae844e081 Fix event encoding issue 2023-02-06 11:10:23 -08:00
William Casarin
140b0e4fc4 Fix bech32 decoding bug
Changelog-Fixed: Fix some lnurls not getting decoded properly
2023-02-06 10:47:13 -08:00
0b476faff7 Fix pluralization of Zaps 2023-02-06 10:13:29 -08:00
Andrii Sievrikov
53ec89551b Add local authentication when accessing private key
Changelog-Added: Use local authentication (faceid) to access private key
2023-02-06 10:10:59 -08:00
Bryan Montz
638052492d Add accessibility labels to EventActionBar
Changelog-Added: Add accessibility labels to action bar
Closes: #530
2023-02-06 10:06:50 -08:00
William Casarin
45f8c37498 build 14 2023-02-06 10:05:14 -08:00
William Casarin
f96ad99790 zaps: initial configuration for default zap amount 2023-02-06 10:05:02 -08:00
Joel Klabo
1f79c20973 Add Missing Contacts Parameter
Closes: #533
2023-02-06 10:04:44 -08:00
e8b23daa3d Fix UX to open relay config view when navigating from personal profile
Changelog-Changed: When on your profile page, open relay view instead for your own relays
Closes: #541
2023-02-06 10:02:18 -08:00
William Casarin
a2eb77a5e9 Hide incoming dms from blocked users
Changelog-Fixed: Hide incoming DMs from blocked users
2023-02-05 10:49:51 -08:00
William Casarin
29a8206586 Hide blocked users from search results
Changelog-Fixed: Hide blocked users from search results
2023-02-05 10:49:18 -08:00
William Casarin
07676a1f95 refactor: should_hide_event -> should_show_event 2023-02-05 10:45:02 -08:00
William Casarin
79ca3b2262 remove orangepill from bootstrap relay list 2023-02-05 10:38:54 -08:00
William Casarin
ba8425dedb Revert "Add remote image loading policy settings"
We still want to blur images from stranges if we set the everyone
policy. This is a regression.

This reverts commit ced5b4974f, reversing
changes made to 9be55b08fd.
2023-02-05 00:24:03 -08:00
Andrii Sievrikov
4faf63f29d Update localization 2023-02-04 23:45:14 -05:00
Andrii Sievrikov
84ad0e03d0 Add local authentication when accessing private key 2023-02-04 23:45:13 -05:00
Rob Seward
7d3d23def3 Add universal link for Cash App
Update the the Cash App wallet option to use a universal link that will start the payment process in Cash App.

Changelog-Fixed: Fix Cash App invoice payments
Closes: #454
2023-02-04 12:56:56 -08:00
252 changed files with 6955 additions and 20874 deletions

View File

@@ -1,3 +1,99 @@
## [1.1.0-3] - 2023-02-20
### Added
- Add a "load more" button instead of always inserting events in timelines (William Casarin)
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
### Changed
- Rename global feed to universe (William Casarin)
- Improve look of post view (ericholguin)
- Added a 20MB content length limit for all image files (OlegAba)
- Improved EventActionBar button spacing (Bryan Montz)
- Polished profile key copy buttons, added animation (Bryan Montz)
- Format large numbers of action bar actions (Joel Klabo)
- Improved blur on images, especially in dark mode (Bryan Montz)
### Fixed
- Remove trailing slash when adding a relay (middlingphys)
- Scroll to top of events instead of the bottom (OlegAba)
- Fix lag on startup when you have lots of DMs (William Casarin)
- Fix an issues where dm notifications appear without any new events (William Casarin)
- Fix some hangs when scrolling by images (OlegAba)
- Force default zap amount text field to accept only numbers (Terry Yiu)
[1.1.0-3]: https://github.com/damus-io/damus/releases/tag/v1.1.0-3
## [1.1.0-2] - 2023-02-14
### Added
- Save drafts to posts, replies and DMs (Terry Yiu)
### Fixed
- Ensure stats get updated in realtime on action bars (William Casarin)
- Fix reposts not getting counted properly (William Casarin)
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
- Fix punctuation getting included in some urls (Gert Goet)
- Improve language detection (Terry Yiu)
- Fix some animated image crashes (William Casarin)
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
## [1.0.0-15] - 2023-02-10
### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin)
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
- Relay Detail View (Joel Klabo)
- Warn when attempting to post an nsec key (Terry Yiu)
- DeepL translation integration (Terry Yiu)
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
- Add accessibility labels to action bar (Bryan Montz)
- Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
### Changed
- Show "Follow Back" button on profile page (William Casarin)
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
- Updated QR code view, include profile image, etc (ericholguin)
- Clicking relay numbers now goes to relay config (radixrat)
### Fixed
- Load zaps, likes and reposts when you open a thread (William Casarin)
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
- Fix sidebar navigation bugs (OlegAba)
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
- Make @ mentions case insensitive (William Casarin)
- Fix some lnurls not getting decoded properly (William Casarin)
- Hide incoming DMs from blocked users (William Casarin)
- Hide blocked users from search results (William Casarin)
- Fix Cash App invoice payments (Rob Seward)
- DM Padding (OlegAba)
- Check for broken lnurls (William Casarin)
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
## [1.0.0-13] - 2023-01-30
### Added
@@ -515,4 +611,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2

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... :)

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;

View File

@@ -11,10 +11,18 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
@@ -28,7 +36,6 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
@@ -39,6 +46,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 */; };
@@ -139,6 +147,8 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
@@ -150,6 +160,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -163,6 +176,14 @@
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; };
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
@@ -185,21 +206,28 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */
@@ -229,7 +257,23 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; };
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -237,6 +281,15 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -249,9 +302,13 @@
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -276,7 +333,6 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
@@ -287,6 +343,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>"; };
@@ -417,6 +474,8 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
@@ -428,6 +487,9 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -443,6 +505,14 @@
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
4CE8795A2996C47A00F758CC /* ZapsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsModel.swift; sourceTree = "<group>"; };
4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; };
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; };
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
@@ -466,20 +536,27 @@
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>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -606,6 +683,7 @@
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
@@ -617,10 +695,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>";
@@ -628,6 +709,9 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
@@ -637,6 +721,7 @@
4CB88387296AF97C00DC99E7 /* ActionBar */,
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
4C363A8728236948006E126D /* BlocksView.swift */,
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
4C285C8128385570008A31F1 /* CarouselView.swift */,
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
4C0A3F90280F6528000448DE /* ChatView.swift */,
@@ -672,7 +757,6 @@
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
647D9A8C2968520300A295DE /* SideMenuView.swift */,
@@ -683,6 +767,7 @@
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -710,9 +795,11 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
4C363A8328233689006E126D /* Parser.swift */,
@@ -733,6 +820,11 @@
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -740,9 +832,14 @@
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
4CE879532996BA0000F758CC /* Detail */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -764,6 +861,15 @@
path = Reactions;
sourceTree = "<group>";
};
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup;
children = (
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
);
path = Profile;
sourceTree = "<group>";
};
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
@@ -781,6 +887,15 @@
path = Events;
sourceTree = "<group>";
};
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup;
children = (
@@ -799,6 +914,7 @@
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -858,6 +974,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
@@ -869,6 +986,9 @@
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -882,6 +1002,31 @@
path = damusUITests;
sourceTree = "<group>";
};
4CE879492995B58700F758CC /* Relays */ = {
isa = PBXGroup;
children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CE879532996BA0000F758CC /* Detail */ = {
isa = PBXGroup;
children = (
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
);
path = Detail;
sourceTree = "<group>";
};
4CE879562996C44A00F758CC /* Zaps */ = {
isa = PBXGroup;
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CEE2AE62804F57B00AB5EEF /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -1028,6 +1173,13 @@
ar,
nl,
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
"zh-HK",
"zh-TW",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1093,6 +1245,7 @@
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1108,10 +1261,12 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
@@ -1120,6 +1275,8 @@
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1134,6 +1291,7 @@
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
@@ -1149,11 +1307,14 @@
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1161,19 +1322,23 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
@@ -1181,6 +1346,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 */,
@@ -1224,27 +1390,35 @@
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
@@ -1253,7 +1427,9 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1261,8 +1437,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
@@ -1270,7 +1448,9 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1315,6 +1495,13 @@
3AC524F0298C000B00693EBF /* ar */,
3A96D41C298DA94500388A2A /* nl */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
3A3040FB29A91F03008A0F29 /* zh-HK */,
3A3040FD29A91F31008A0F29 /* zh-TW */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1333,6 +1520,13 @@
3AC524EE298C000B00693EBF /* ar */,
3A96D41A298DA94500388A2A /* nl */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
3A3040F929A91ED6008A0F29 /* zh-HK */,
3A3040FC29A91F31008A0F29 /* zh-TW */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1351,6 +1545,13 @@
3AC524EF298C000B00693EBF /* ar */,
3A96D41B298DA94500388A2A /* nl */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
3A3040FA29A91EFC008A0F29 /* zh-HK */,
3A3040FE29A91F31008A0F29 /* zh-TW */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1486,7 +1687,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1494,6 +1695,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1508,7 +1710,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1527,7 +1729,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1535,6 +1737,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1549,7 +1752,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;

View File

@@ -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"
}
},
{

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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" : {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "profile-banner.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {

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" : {

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" : {

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" : {

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" : {

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" : {

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" : {

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" : {

View File

@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "encrypted-message.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {

View File

@@ -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

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

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" : {

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" : {

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" : {

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" : {

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" : {

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" : {

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" : {

View File

@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "walletofsatoshi.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {

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" : {

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" : {

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
}

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)
}
}

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)
}
}
@@ -71,10 +68,7 @@ struct TranslateView: View {
}
}
.task {
let translate_url = damus_state.settings.libretranslate_url
let api_key = damus_state.settings.libretranslate_api_key
guard noteLanguage == nil && !checkingTranslationStatus && translate_url != "" else {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
@@ -86,16 +80,22 @@ struct TranslateView: View {
currentLanguage = Locale.current.languageCode ?? "en"
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in.
let content = event.get_content(damus_state.keypair.privkey)
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(damus_state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
if let lang = noteLanguage, noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language.
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
} else {
noteLanguage = Locale.canonicalLanguageIdentifier(from: lang)
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
}
}
@@ -109,8 +109,15 @@ struct TranslateView: View {
if note_lang != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
let translator = Translator(translate_url, apiKey: api_key)
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
if originalContent == translated_note {
// If the translation is the same as the original, don't bother showing it.
noteLanguage = currentLanguage
translated_note = nil
}
} catch {
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
noteLanguage = currentLanguage
@@ -120,9 +127,8 @@ struct TranslateView: View {
if let translated = translated_note {
// Render translated note.
let blocks = event.get_blocks(content: translated)
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
translated_artifacts = render_blocks(blocks: blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, show_images: show_images)
let translatedBlocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
@@ -134,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)
}
}

View File

@@ -17,7 +17,7 @@ struct UserView: View {
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, contacts: damus_state.contacts)
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)

View File

@@ -52,7 +52,8 @@ struct ZapButton: View {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: 1000000) 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
}
@@ -99,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)
@@ -107,11 +108,13 @@ struct ZapButton: View {
send_zap()
}
}
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
.offset(x: 22)
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
if bar.zap_total > 0 {
Text(verbatim: format_msats_abbrev(bar.zap_total))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)

View File

@@ -7,13 +7,12 @@
import SwiftUI
import Starscream
import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://relay.snort.social",
"wss://nostr.orangepill.dev",
"wss://offchain.pub",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
@@ -28,12 +27,16 @@ enum Sheets: Identifiable {
case post
case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
}
}
@@ -89,7 +92,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -133,7 +136,37 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
}
}
func popToRoot() {
profile_open = false
thread_open = false
search_open = false
isSideBarOpened = false
}
var timelineNavItem: some View {
VStack {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
Text(verbatim: "")
}
}
}
@@ -157,9 +190,10 @@ struct ContentView: View {
PostingTimelineView
case .notifications:
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
VStack(spacing: 0) {
Divider()
TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
}
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
@@ -168,27 +202,12 @@ struct ContentView: View {
EmptyView()
}
}
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Universe 🛸", comment: "Navigation bar title for universal view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
}
.ignoresSafeArea(.keyboard)
@@ -197,7 +216,7 @@ struct ContentView: View {
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
} else {
EmptyView()
}
@@ -245,15 +264,18 @@ struct ContentView: View {
if let damus = self.damus_state {
NavigationView {
ZStack {
VStack {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, contacts: damus_state!.contacts)
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -265,19 +287,28 @@ struct ContentView: View {
.foregroundColor(.gray)
}
}
// maybe expand this to other timelines in the future
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
}
}
}
}
}
Color.clear
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened)
)
.tabViewStyle(.page(indexDisplayMode: .never))
}
.navigationBarHidden(isSideBarOpened ? true: false) // Would prefer a different way of doing this.
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
}
.navigationViewStyle(.stack)
@@ -287,7 +318,6 @@ struct ContentView: View {
}
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
setup_notifications()
}
.sheet(item: $active_sheet) { item in
@@ -298,6 +328,17 @@ struct ContentView: View {
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
if #available(iOS 16.0, *) {
RelayFilterView(state: damus_state!, timeline: timeline)
.presentationDetents([.height(550)])
.presentationDragIndicator(.visible)
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
}
}
.onOpenURL { url in
@@ -416,6 +457,8 @@ struct ContentView: View {
let post_res = obj.object as! NostrPostResult
switch post_res {
case .post(let post):
//let post = tup.0
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
self.damus_state?.pool.send(.event(new_ev))
@@ -520,6 +563,7 @@ struct ContentView: View {
}
func switch_timeline(_ timeline: Timeline) {
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
if timeline == self.selected_timeline {
@@ -545,24 +589,33 @@ struct ContentView: View {
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in BOOTSTRAP_RELAYS {
add_relay(pool, relay)
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: UserSettingsStore()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts()
)
home.damus_state = self.damus_state!

View File

@@ -24,7 +24,6 @@
<string>zeusln</string>
<string>zebedee</string>
<string>lightning</string>
<string>squarecash</string>
<string>phoenix</string>
<string>lnlink</string>
<string>strike</string>

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
}

View File

@@ -0,0 +1,50 @@
//
// BookmarksManager.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import Foundation
class BookmarksManager {
private let userDefaults = UserDefaults.standard
private let pubkey: String
init(pubkey: String) {
self.pubkey = pubkey
}
var bookmarks: [String] {
get {
return userDefaults.stringArray(forKey: storageKey()) ?? []
}
set {
let uniqueBookmarks = Array(Set(newValue))
if uniqueBookmarks != bookmarks {
userDefaults.set(uniqueBookmarks, forKey: storageKey())
}
}
}
func isBookmarked(_ string: String) -> Bool {
return bookmarks.contains(string)
}
func updateBookmark(_ string: String) {
if isBookmarked(string) {
bookmarks = bookmarks.filter { $0 != string }
} else {
bookmarks.append(string)
}
}
func clearAll() {
bookmarks = []
}
private func storageKey() -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
}

View File

@@ -21,6 +21,9 @@ struct DamusState {
let zaps: Zaps
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
var pubkey: String {
return keypair.pubkey
@@ -32,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())
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())
}
}

View File

@@ -0,0 +1,35 @@
//
// DeepLPlan.swift
// damus
//
// Created by Terry Yiu on 2/3/23.
//
import Foundation
enum DeepLPlan: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
var url: String
}
case free
case pro
var model: Model {
switch self {
case .free:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
case .pro:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

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 = ""
}
}

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] = [:]
}

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)
}
}
}

View File

@@ -51,11 +51,7 @@ class FollowersModel: ObservableObject {
if has_contact.contains(ev.pubkey) {
return
}
process_contact_event(
pool: damus_state.pool,
contacts: damus_state.contacts,
pubkey: damus_state.pubkey, ev: ev
)
process_contact_event(state: damus_state, ev: ev)
contacts?.append(ev.pubkey)
has_contact.insert(ev.pubkey)
}

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) {
@@ -117,14 +132,18 @@ class HomeModel: ObservableObject {
}
}
func handle_zap_event_with_zapper(_ ev: NostrEvent, zapper: String) {
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
damus_state.zaps.add_zap(zap: zap)
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
guard zap.target.pubkey == our_pubkey else {
return
}
if !notifications.insert(ev) {
return
}
@@ -138,16 +157,12 @@ class HomeModel: ObservableObject {
return
}
guard ptag == damus_state.pubkey else {
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
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) {
handle_zap_event_with_zapper(ev, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
@@ -161,7 +176,8 @@ class HomeModel: ObservableObject {
}
DispatchQueue.main.async {
self.handle_zap_event_with_zapper(ev, zapper: zapper)
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
}
}
@@ -179,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) {
@@ -193,7 +209,7 @@ class HomeModel: ObservableObject {
}
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
process_contact_event(state: self.damus_state, ev: ev)
if sub_id == init_subid {
pool.send(.unsubscribe(init_subid), to: [relay_id])
@@ -229,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)
}
}
@@ -246,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)
}
}
@@ -286,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
@@ -300,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
@@ -442,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)
}
@@ -455,29 +474,45 @@ 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) {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
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(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
}
}
}
@@ -623,14 +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))
}
@@ -643,31 +678,31 @@ func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk
}
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
guard ev.pubkey == pubkey else {
func load_our_stuff(state: DamusState, ev: NostrEvent) {
guard ev.pubkey == state.pubkey else {
return
}
// only use new stuff
if let current_ev = contacts.event {
if let current_ev = state.contacts.event {
guard ev.created_at > current_ev.created_at else {
return
}
}
let m_old_ev = contacts.event
contacts.event = ev
let m_old_ev = state.contacts.event
state.contacts.event = ev
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
}
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
add_contact_if_friend(contacts: contacts, ev: ev)
func process_contact_event(state: DamusState, ev: NostrEvent) {
load_our_stuff(state: state, ev: ev)
add_contact_if_friend(contacts: state.contacts, ev: ev)
}
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
@@ -691,14 +726,15 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
let diff = old.symmetricDifference(new)
let new_relay_filters = load_relay_filters(state.pubkey) == nil
for d in diff {
changed = true
if new.contains(d) {
if let url = URL(string: d) {
try? pool.add_relay(url, info: decoded[d] ?? .rw)
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
}
} else {
pool.remove_relay(d)
state.pool.remove_relay(d)
}
}
@@ -707,9 +743,61 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
}
}
func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
let relay_id = url.absoluteString
guard metadatas.lookup(relay_id: relay_id) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
DispatchQueue.main.async {
metadatas.insert(relay_id: relay_id, metadata: meta)
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
guard let url = URL(string: urlString) else {
return nil
}
var request = URLRequest(url: url)
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
var res: (Data, URLResponse)? = nil
res = try await URLSession.shared.data(for: request)
guard let data = res?.0 else {
return nil
}
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
return nip11
}
func process_relay_metadata() {
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
@@ -736,15 +824,34 @@ func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, ou
}
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
}
@@ -781,10 +888,10 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
}
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return true
return false
}
return !ev.should_show_event
return ev.should_show_event
}

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)
}
}

View File

@@ -17,7 +17,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var url: String?
}
case none
case argosopentech
case terraprint
case vern
@@ -25,8 +24,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
case .argosopentech:
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:

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
@@ -237,17 +263,19 @@ func format_msats_abbrev(_ msats: Int64) -> String {
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64) -> String {
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
numberFormatter.locale = locale
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "sats_count", value: nil, table: nil), locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {

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
@@ -20,6 +20,24 @@ class ProfileModel: ObservableObject, Equatable {
var sub_id = UUID().description
var prof_subid = UUID().description
func follows(pubkey: String) -> Bool {
guard let contacts = self.contacts else {
return false
}
for tag in contacts.tags {
guard tag.count >= 2 && tag[0] == "p" else {
continue
}
if tag[1] == pubkey {
return true
}
}
return false
}
func get_follow_target() -> FollowTarget {
if let contacts = contacts {
return .contact(contacts)
@@ -70,7 +88,7 @@ class ProfileModel: ObservableObject, Equatable {
}
func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
process_contact_event(state: damus, ev: ev)
// only use new stuff
if let current_ev = self.contacts {
@@ -93,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable {
return
}
if ev.is_textlike || ev.known_kind == .boost {
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
if self.events.insert(ev) {
self.objectWillChange.send()
}
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {

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)
}
}

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)
}
}

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,12 +31,14 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
loading = true
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
}
func unsubscribe(to: String? = nil) {
@@ -54,14 +56,14 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}
seen_pubkey.insert(ev.pubkey)
let _ = insert_uniq_sorted_event(events: &events, new_ev: ev) {
$0.created_at > $1.created_at
if self.events.insert(ev) {
self.objectWillChange.send()
}
}
case .notice(let msg):
@@ -74,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime
unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state)
}

View File

@@ -9,20 +9,27 @@ import Foundation
class SearchModel: ObservableObject {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
@Published var channel_name: String? = nil
let pool: RelayPool
var search: NostrFilter
let contacts: Contacts
let sub_id = UUID().description
let limit: UInt32 = 500
init(pool: RelayPool, search: NostrFilter) {
init(contacts: Contacts, pool: RelayPool, search: NostrFilter) {
self.contacts = contacts
self.pool = pool
self.search = search
}
func filter_muted() {
self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
// since 1 month
search.limit = self.limit
@@ -47,7 +54,11 @@ class SearchModel: ObservableObject {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
guard should_show_event(contacts: contacts, ev: ev) else {
return
}
if self.events.insert(ev) {
objectWillChange.send()
}
}
@@ -88,6 +99,21 @@ func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
return false
}
func tag_is_hashtag(_ tag: [String]) -> Bool {
// "hashtag" is deprecated, will remove in the future
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
}
func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool {
for tag in tags {
if tag_is_hashtag(tag) && tag[1] == hashtag {
return true
}
}
return false
}
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
if let hashtags = filter.hashtag {
return event_matches_hashtag(ev, hashtags: hashtags)

View File

@@ -0,0 +1,37 @@
//
// TranslationService.swift
// damus
//
// Created by Terry Yiu on 2/3/23.
//
import Foundation
enum TranslationService: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
}
case none
case libretranslate
case deepl
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
case .libretranslate:
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
case .deepl:
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

View File

@@ -12,6 +12,29 @@ func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
func pk_setting_key(_ pubkey: String, key: String) -> String {
return "\(pubkey)_\(key)"
}
func default_zap_setting_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "default_zap_amount")
}
func set_default_zap_amount(pubkey: String, amount: Int) {
let key = default_zap_setting_key(pubkey: pubkey)
UserDefaults.standard.setValue(amount, forKey: key)
}
func get_default_zap_amount(pubkey: String) -> Int? {
let key = default_zap_setting_key(pubkey: pubkey)
let amt = UserDefaults.standard.integer(forKey: key)
if amt == 0 {
return nil
}
return amt
}
func get_default_wallet(_ pubkey: String) -> Wallet {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
@@ -22,7 +45,23 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
}
}
func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
private func get_translation_service(_ pubkey: String) -> TranslationService? {
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
return nil
}
return TranslationService(rawValue: translation_service)
}
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
return nil
}
return DeepLPlan(rawValue: server_name)
}
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
return nil
}
@@ -30,7 +69,7 @@ func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
return LibreTranslateServer(rawValue: server_name)
}
func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
}
@@ -57,6 +96,32 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
}
@Published var deepl_plan: DeepLPlan {
didSet {
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
}
}
@Published var deepl_api_key: String {
didSet {
do {
if deepl_api_key == "" {
try clearDeepLApiKey()
} else {
try saveDeepLApiKey(deepl_api_key)
}
} catch {
// No-op.
}
}
}
@Published var libretranslate_server: LibreTranslateServer {
didSet {
if oldValue == libretranslate_server {
@@ -67,7 +132,7 @@ class UserSettingsStore: ObservableObject {
libretranslate_api_key = ""
if libretranslate_server == .custom || libretranslate_server == .none {
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
@@ -102,18 +167,25 @@ class UserSettingsStore: ObservableObject {
show_wallet_selector = should_show_wallet_selector(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
if let server = get_libretranslate_server(pubkey) {
self.libretranslate_server = server
self.libretranslate_url = get_libretranslate_url(pubkey, server: server) ?? ""
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
if let translation_service = get_translation_service(pubkey) {
self.translation_service = translation_service
} else {
// Note from @tyiu:
// Default server is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
libretranslate_server = .none
self.translation_service = .none
}
if let libretranslate_server = get_libretranslate_server(pubkey) {
self.libretranslate_server = libretranslate_server
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
} else {
// Choose a random server to distribute load.
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
libretranslate_url = ""
}
@@ -122,15 +194,46 @@ class UserSettingsStore: ObservableObject {
} catch {
libretranslate_api_key = ""
}
if let deepl_plan = get_deepl_plan(pubkey) {
self.deepl_plan = deepl_plan
} else {
self.deepl_plan = .free
}
do {
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
} catch {
deepl_api_key = ""
}
}
func saveLibreTranslateApiKey(_ apiKey: String) throws {
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
func clearLibreTranslateApiKey() throws {
private func clearLibreTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
private func saveDeepLApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
}
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
@@ -138,3 +241,9 @@ struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
var accessGroup: String? = nil
var accountName = "libretranslate_apikey"
}
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "deepl_apikey"
}

View File

@@ -45,7 +45,7 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
case .cashapp:
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "squarecash://",
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
case .muun:
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")

View File

@@ -0,0 +1,83 @@
//
// ZapsModel.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import Foundation
class ZapsModel: ObservableObject {
let state: DamusState
let target: ZapTarget
var zaps: [Zap]
let zaps_subid = UUID().description
let profiles_subid = UUID().description
init(state: DamusState, target: ZapTarget) {
self.state = state
self.target = target
self.zaps = []
}
func subscribe() {
var filter = NostrFilter.filter_kinds([9735])
switch target {
case .profile(let profile_id):
filter.pubkeys = [profile_id]
case .note(let note_target):
filter.referenced_ids = [note_target.note_id]
}
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: zaps_subid)
}
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
guard case .nostr_event(let resp) = conn_ev else {
return
}
guard resp.subid == zaps_subid else {
return
}
switch resp {
case .notice:
break
case .eose:
let events = self.zaps.map { $0.request.ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
}
}
}
}

View File

@@ -102,7 +102,7 @@ struct Profile: Codable {
}
var lnurl: String? {
guard let addr = lud06 ?? lud16 else {
guard let addr = lud16 ?? lud06 else {
return nil;
}

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) {
@@ -370,6 +356,7 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
func encode_json<T: Encodable>(_ val: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
}

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 {

View File

@@ -24,6 +24,30 @@ enum RelayFlags: Int {
case broken = 1
}
struct Limitations: Codable {
let payment_required: Bool?
static var empty: Limitations {
Limitations(payment_required: nil)
}
}
struct RelayMetadata: Codable {
let name: String?
let description: String?
let pubkey: String?
let contact: String?
let supported_nips: [Int]?
let software: String?
let version: String?
let limitation: Limitations?
let payments_url: String?
var is_paid: Bool {
return limitation?.payment_required ?? false
}
}
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection

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? {
@@ -118,17 +128,16 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
}
func make_nostr_push_event(ev: NostrEvent) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
let event_data = try! encoder.encode(ev)
let event = String(decoding: event_data, as: UTF8.self)
guard let event = encode_json(ev) else {
return nil
}
let encoded = "[\"EVENT\",\(event)]"
print(encoded)
return encoded
}
func make_nostr_unsubscribe_req(_ sub_id: String) -> String? {
return "[\"CLOSE\",\"\(sub_id)\"]"
"[\"CLOSE\",\"\(sub_id)\"]"
}
func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? {
@@ -145,10 +154,3 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
req += "]"
return req
}
func make_websocket(url: URL) -> WebSocket {
let req = URLRequest(url: url)
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
return WebSocket(request: req, compressionHandler: .none)
}

View File

@@ -42,6 +42,8 @@ class RelayPool {
var relays: [Relay] = []
var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = []
var seen: Set<String> = Set()
var counts: [String: UInt64] = [:]
var descriptors: [RelayDescriptor] {
relays.map { $0.descriptor }
@@ -149,9 +151,9 @@ class RelayPool {
self.send(.unsubscribe(sub_id), to: to)
}
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) {
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
register_handler(sub_id: sub_id, handler: handler)
send(.subscribe(.init(filters: filters, sub_id: sub_id)))
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
}
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
@@ -193,27 +195,13 @@ class RelayPool {
relay.connection.send(req)
}
}
func get_relays(_ ids: [String]) -> [Relay] {
var relays: [Relay] = []
for id in ids {
if let relay = get_relay(id) {
relays.append(relay)
}
}
return relays
relays.filter { ids.contains($0.id) }
}
func get_relay(_ id: String) -> Relay? {
for relay in relays {
if relay.id == id {
return relay
}
}
return nil
relays.first(where: { $0.id == id })
}
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
@@ -241,8 +229,25 @@ class RelayPool {
}
}
func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev {
let k = relay_id + nev.id
if !seen.contains(k) {
seen.insert(k)
if counts[relay_id] == nil {
counts[relay_id] = 1
} else {
counts[relay_id] = (counts[relay_id] ?? 0) + 1
}
}
}
}
}
func handle_event(relay_id: String, event: NostrConnectionEvent) {
record_last_pong(relay_id: relay_id, event: event)
record_seen(relay_id: relay_id, event: event)
// run req queue when we reconnect
if case .ws_event(let ws) = event {

View File

@@ -143,11 +143,12 @@ func eightToFiveBits(_ input: [UInt8]) -> [UInt8] {
}
/// Decode Bech32 string
@discardableResult
public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
guard let strBytes = str.data(using: .utf8) else {
throw Bech32Error.nonUTF8String
}
guard strBytes.count <= 90 else {
guard strBytes.count <= 2024 else {
throw Bech32Error.stringLengthExceeded
}
var lower: Bool = false

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!)
}
}

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 = []
}
}

View File

@@ -29,7 +29,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
}
func end_editing() {
public func end_editing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}

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

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()
}
}

View File

@@ -158,6 +158,20 @@ func get_saved_privkey() -> String? {
return mkey.map { $0.trimmingCharacters(in: .whitespaces) }
}
/**
Detects whether a string might contain an nsec1 prefixed private key.
It does not determine if it's the current user's private key and does not verify if it is properly encoded or has the right length.
*/
func contentContainsPrivateKey(_ content: String) -> Bool {
if #available(iOS 16.0, *) {
return content.contains(/nsec1[02-9ac-z]+/)
} else {
let regex = try! NSRegularExpression(pattern: "nsec1[02-9ac-z]+")
return (regex.firstMatch(in: content, range: NSRange(location: 0, length: content.count)) != nil)
}
}
fileprivate func removePrivateKeyFromUserDefaults() throws {
guard let privKey = UserDefaults.standard.string(forKey: "privkey") else { return }
try save_privkey(privkey: privKey)

View File

@@ -10,10 +10,11 @@ import LinkPresentation
class CustomLinkView: LPLinkView {
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}
enum Metadata {
case linkmeta(LPLinkMetadata)
case linkmeta(CachedMetadata)
case url(URL)
}
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CustomLinkView {
switch meta {
case .linkmeta(let linkmeta):
return CustomLinkView(metadata: linkmeta)
return CustomLinkView(metadata: linkmeta.meta)
case .url(let url):
return CustomLinkView(url: url)
}
}
func updateUIView(_ uiView: CustomLinkView, context: Context) {
switch meta {
case .linkmeta(let cached):
cached.intrinsic_height = uiView.intrinsicContentSize.height
case .url:
return
}
}
}

View File

@@ -0,0 +1,19 @@
//
// LocalizationUtil.swift
// damus
//
// Created by Terry Yiu on 2/24/23.
//
import Foundation
func bundleForLocale(locale: Locale) -> Bundle {
let path = Bundle.main.path(forResource: locale.identifier, ofType: "lproj")
return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main
}
func formatInt(_ int: Int) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
return numberFormatter.string(from: NSNumber(integerLiteral: int)) ?? "\(int)"
}

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

View File

@@ -0,0 +1,26 @@
//
// NIPURLBuilder.swift
// damus
//
// Created by Honk on 2/1/23.
//
import Foundation
struct NIPURLBuilder {
static private let baseURL = "https://github.com/nostr-protocol/nips/blob/master/"
static func url(forNIP nip: Int) -> URL? {
let urlString = baseURL + "\(formatNipNumber(nip: nip)).md"
return URL(string: urlString)
}
static func formatNipNumber(nip: Int) -> String {
let formatted: String
if nip < 10 {
formatted = "0\(nip)"
} else {
formatted = "\(nip)"
}
return formatted
}
}

View File

@@ -98,6 +98,12 @@ extension Notification.Name {
static var deleted_account: Notification.Name {
return Notification.Name("deleted_account")
}
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
static var update_bookmarks: Notification.Name {
return Notification.Name("update_bookmarks")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {

View File

@@ -8,8 +8,18 @@
import Foundation
import LinkPresentation
class CachedMetadata {
let meta: LPLinkMetadata
var intrinsic_height: CGFloat?
init(meta: LPLinkMetadata) {
self.meta = meta
self.intrinsic_height = nil
}
}
enum Preview {
case value(LinkViewRepresentable)
case value(CachedMetadata)
case failed
}
@@ -20,12 +30,12 @@ class PreviewCache {
return previews[evid]
}
func store(evid: String, preview: LinkViewRepresentable?) {
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
previews[evid] = .failed
case .some(let meta):
previews[evid] = .value(meta)
previews[evid] = .value(CachedMetadata(meta: meta))
}
}

View File

@@ -0,0 +1,94 @@
//
// RelayFilters.swift
// damus
//
// Created by William Casarin on 2023-02-08.
//
import Foundation
struct RelayFilter: Hashable {
let timeline: Timeline
let relay_id: String
init(timeline: Timeline, relay_id: String, on: Bool = false) {
self.timeline = timeline
self.relay_id = relay_id
}
}
class RelayFilters {
private let our_pubkey: String
private var disabled: Set<RelayFilter>
func is_filtered(timeline: Timeline, relay_id: String) -> Bool {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
let contains = disabled.contains(filter)
return contains
}
func remove(timeline: Timeline, relay_id: String) {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
if !disabled.contains(filter) {
return
}
disabled.remove(filter)
save_relay_filters(our_pubkey, filters: disabled)
}
func insert(timeline: Timeline, relay_id: String) {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
if disabled.contains(filter) {
return
}
disabled.insert(filter)
save_relay_filters(our_pubkey, filters: disabled)
}
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
disabled = load_relay_filters(our_pubkey) ?? Set()
}
}
func save_relay_filters(_ pubkey: String, filters: Set<RelayFilter>) {
let key = pk_setting_key(pubkey, key: "relay_filters")
let arr = Array(filters.map { filter in "\(filter.timeline)\t\(filter.relay_id)" })
UserDefaults.standard.set(arr, forKey: key)
}
func relay_filter_setting_key(_ pubkey: String) -> String {
return pk_setting_key(pubkey, key: "relay_filters")
}
func clear_relay_filters(_ pubkey: String) {
let key = relay_filter_setting_key(pubkey)
UserDefaults.standard.removeObject(forKey: key)
}
func load_relay_filters(_ pubkey: String) -> Set<RelayFilter>? {
let key = relay_filter_setting_key(pubkey)
guard let filters = UserDefaults.standard.stringArray(forKey: key) else {
return nil
}
return filters.reduce(into: Set()) { s, str in
let parts = str.components(separatedBy: "\t")
guard parts.count == 2 else {
return
}
guard let timeline = Timeline.init(rawValue: parts[0]) else {
return
}
let filter = RelayFilter(timeline: timeline, relay_id: parts[1])
s.insert(filter)
}
}
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] {
return pool.descriptors
.map { $0.url.absoluteString }
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
}

View File

@@ -0,0 +1,20 @@
//
// RelayMetadatas.swift
// damus
//
// Created by William Casarin on 2023-02-09.
//
import Foundation
class RelayMetadatas {
private var metadata: [String: RelayMetadata] = [:]
func lookup(relay_id: String) -> RelayMetadata? {
return metadata[relay_id]
}
func insert(relay_id: String, metadata: RelayMetadata) {
self.metadata[relay_id] = metadata
}
}

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
}
}

View File

@@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current
return formatter.string(from: DateComponents(calendar: calendar, second: second))!
}
return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred")
let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current)
return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred")
}

127
damus/Util/Translator.swift Normal file
View File

@@ -0,0 +1,127 @@
//
// Translator.swift
// damus
//
// Created by Terry Yiu on 2/4/23.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct Translator {
private let userSettingsStore: UserSettingsStore
private let session = URLSession.shared
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(_ userSettingsStore: UserSettingsStore) {
self.userSettingsStore = userSettingsStore
}
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return text
}
}
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
struct RequestBody: Encodable {
let q: String
let source: String
let target: String
let api_key: String?
}
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.libretranslate_api_key)
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
return response.translatedText
}
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
let url = try makeURL(userSettingsStore.deepl_plan.model.url, path: "/v2/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("DeepL-Auth-Key \(userSettingsStore.deepl_api_key)", forHTTPHeaderField: "Authorization")
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translations: [DeepLTranslations]
}
struct DeepLTranslations: Decodable {
let detected_source_language: String
let text: String
}
let response: Response = try await decodedData(for: request)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
guard var components = URLComponents(string: baseUrl) else {
throw URLError(.badURL)
}
components.path = path
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
let data = try await session.data(for: request)
let result = try decoder.decode(Output.self, from: data)
return result
}
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
let onCancel = { task?.cancel() }
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
task = dataTask(with: request) { data, _, error in
guard let data = data else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: data)
}
task?.resume()
}
},
onCancel: { onCancel() }
)
}
}

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