Compare commits

...

393 Commits

Author SHA1 Message Date
77bcd1b715 Fix localization and add tests for EventGroupView 2023-02-27 10:20:50 +13:00
William Casarin
64b1a57918 Notifications
Changelog-Added: Add new Notifications View
2023-02-25 11:33:25 -08:00
William Casarin
e4dd585754 Fix text color in dark mode on post view 2023-02-24 15:11:09 -08:00
Swift
436d20dfbd Rich tagging
Changelog-Changed: No more inline npubs when tagging users
Closes: #691
2023-02-24 13:35:33 -08:00
OlegAba
810b3e1fa5 Fix mention rounded border
Closes: #670
2023-02-24 13:04:36 -08:00
OlegAba
75fb0d19e2 Fix ImageCarousel corner radius and context menu 2023-02-24 13:04:13 -08:00
OlegAba
a2749eaaaa Fix event dividers 2023-02-24 13:04:11 -08:00
OlegAba
83c9289345 Lazy loading of thread child events
Closes: #679
2023-02-24 12:49:26 -08:00
Joel Klabo
4c3a83772e Update Alignment of Side Menu Labels
Changelog-Fixed: Fix alignment of side menu labels
Closes: #688
2023-02-24 12:45:47 -08:00
5cd4c2d75e Fix localization issues, add tests, import translations, and add zh-CN and zh-TW
Closes: #689
2023-02-24 12:44:54 -08:00
Joel Klabo
85e797a054 Embed in ScrollView 2023-02-24 12:42:49 -08:00
Joel Klabo
9f52e2c246 Fix Identical Participants in ParticipantView
Changelog-Fixed: Fix duplicated participants in reply-to view
Closes: #685
2023-02-24 12:42:18 -08: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
Joel Klabo
ac1a5d237e Add Copy Invoice Button
Changelog-Added: Copy invoice button
Closes: #469
2023-02-04 12:52:57 -08:00
William Casarin
cfcd799d63 Fix build 2023-02-04 12:23:24 -08:00
OlegAba
351b32308f Fix DM view padding
Changelog-Fixed: DM Padding
Closes: #476
2023-02-04 12:22:08 -08:00
Hanton Yang
5a4299edaa Fix text truncation in CarouselItemView
Closes: #481
2023-02-04 12:21:21 -08:00
ericholguin
99b619e011 Updated QR Code view, include profile image, name, and remove pubkey text
Closes: #443
Changelog-Changed: Updated QR code view, include profile image, etc
2023-02-04 12:11:20 -08:00
William Casarin
d5ee9e4780 Revert "Lightweight png files"
This reverts commit 71acb16387.
2023-02-04 12:07:42 -08:00
radixrat
ced5b4974f Add remote image loading policy settings
Changelog-Added: Ability to change remote image loading policy
2023-02-04 11:44:41 -08:00
9be55b08fd Fix profile edit button text to not wrap
Closes: #500
2023-02-04 10:39:57 -08:00
Peer Richelsen
ac5f39a922 replace testflight with new app store link
Closes: #503
2023-02-04 10:39:42 -08:00
Joel Klabo
0e9691ae7a Add Test Status Badge
Closes: #512
2023-02-04 10:39:30 -08:00
1441d339a7 Export and import translations, remove de_AT in favor of de, and move zh to zh-CN
Closes: #515
2023-02-04 10:35:32 -08:00
Alex
2517132041 Remove brackets in the image example
Closes: #518
2023-02-04 10:34:56 -08:00
pea-sys
71acb16387 Lightweight png files
Changelog-Changed: Make app smaller by optimizing pngs
Closes: #507
2023-02-04 10:09:33 -08:00
William Casarin
9e2e8595e8 move text event to its own file, improve zaps 2023-02-04 10:01:37 -08:00
radixrat
1a2e9464af add friends of friends, apply to all images 2023-02-03 19:37:23 -05:00
Thomas
63dd39c7e4 Using enum 2023-02-03 19:37:23 -05:00
radixrat
40be9885c5 add remote loading image setting 2023-02-03 19:37:23 -05:00
William Casarin
331d7e9792 make lnurl sanity check case insensitive 2023-02-03 11:41:31 -08:00
William Casarin
d21613a765 removed extra divider 2023-02-03 11:29:45 -08:00
William Casarin
7780120504 ensure lnurls are actually lnurls
Changelog-Fixed: Check for broken lnurls
2023-02-03 11:29:11 -08:00
William Casarin
1696e0365e refactor: settings and translation view 2023-02-03 09:25:07 -08:00
William Casarin
006f8d79e0 Lightning Zaps
Added initial lightning zaps/tipping integration

Changelog-Added: Receive Lightning Zaps
2023-02-02 15:51:57 -08:00
Suhail Saqan
135432e03c Allow text selection in bio
Changelog-Added: Allow text selection in bio
Closes: #494
2023-02-02 15:49:57 -08:00
William Casarin
1fd4d4d950 refactor: move translate button into its own view 2023-02-02 14:13:07 -08:00
7d406fd75f Replace LibreTranslate detect server call with Apple's Natural Language library
Closes: #482
2023-02-02 13:54:14 -08:00
radixrat
0902548336 Clicking on relay numbers on home view brings you to config
Changelog-Changed: Clicking relay numbers now goes to relay config
Closes: #491
2023-02-02 13:48:46 -08:00
William Casarin
09547529ad Revert "Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_nl' into translations"
This reverts commit 33368c3ac4, reversing
changes made to 99d282ee20.
2023-02-02 13:45:13 -08:00
William Casarin
6bd7e7563c Merge branch 'translations' 2023-02-02 09:56:36 -08:00
William Casarin
5ec77bf8d2 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_de' into translations 2023-02-02 09:55:57 -08:00
William Casarin
33368c3ac4 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_nl' into translations 2023-02-02 09:55:48 -08:00
William Casarin
99d282ee20 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_fr_FR' into translations 2023-02-02 09:54:51 -08:00
William Casarin
a9009049c9 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_pl_PL' into translations 2023-02-02 09:54:44 -08:00
William Casarin
e64abca1f0 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_pt_PT' into translations 2023-02-02 09:54:23 -08:00
William Casarin
e90408027b Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_es_419' into translations 2023-02-02 09:38:30 -08:00
William Casarin
58a74af25b Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_de_AT' into translations 2023-02-02 09:38:16 -08:00
William Casarin
0a33f4ca1c Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_ar' into translations 2023-02-02 09:37:50 -08:00
William Casarin
960ed8158c Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_it_IT' into translations 2023-02-02 09:37:17 -08:00
0cff4dc194 Import zh translations 2023-02-02 11:42:22 -05:00
transifex-integration[bot]
03822418c7 Apply translations in zh
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'zh' language.
2023-02-02 16:40:55 +00:00
de510423f6 Import it_IT translations 2023-02-02 10:38:16 -05:00
transifex-integration[bot]
264fbac16c Apply translations in it_IT
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'it_IT' language.
2023-02-02 15:36:26 +00:00
2cd508c4c2 Import ar translations 2023-02-02 09:28:41 -05:00
5e0b4583c0 Import de_AT translations 2023-01-31 20:17:21 -05:00
4d2a670c72 Import es_419 translations 2023-01-31 20:16:59 -05:00
73d17ac708 Import pt_PT translations 2023-01-31 20:16:30 -05:00
c2e955faa5 Import pl_PL translations 2023-01-31 20:16:07 -05:00
58d95a0c15 Import fr_FR translations 2023-01-31 20:15:36 -05:00
d86a6a9e16 Import nl translations 2023-01-31 20:15:09 -05:00
1269c00485 Import de translations 2023-01-31 20:14:27 -05:00
transifex-integration[bot]
98183cb4a8 Apply translations in de_AT
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-02-01 01:13:12 +00:00
transifex-integration[bot]
537100d923 Apply translations in es_419
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'es_419' language.
2023-02-01 01:13:00 +00:00
transifex-integration[bot]
ca3c65496a Apply translations in pt_PT
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-02-01 01:12:48 +00:00
transifex-integration[bot]
9b2fb867b4 Apply translations in pl_PL
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pl_PL' language.
2023-02-01 01:12:37 +00:00
transifex-integration[bot]
52f6dff4e9 Apply translations in fr_FR
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-02-01 01:12:14 +00:00
transifex-integration[bot]
94811b3737 Apply translations in nl
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'nl' language.
2023-02-01 01:12:02 +00:00
transifex-integration[bot]
921b5a2a31 Apply translations in de
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de' language.
2023-02-01 01:11:51 +00:00
transifex-integration[bot]
116825b556 Apply translations in ar
translated for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-02-01 01:11:39 +00:00
William Casarin
e40cc9a50a v1.0.0-13 changelog 2023-01-30 16:46:43 -08:00
William Casarin
43f6053429 v1.0.0-13 2023-01-30 16:45:58 -08:00
William Casarin
1e8d8120ac Switch up the bootstrap relays 2023-01-30 16:45:22 -08:00
William Casarin
dfb681cc02 Fix profile action sheet on ipad
Changelog-Fixed: Fix hidden profile action sheet when clicking ...
2023-01-30 16:39:52 -08:00
Jonathan Milligan
889c584487 fix: Redundant logout button in config view
Since there's now an easy to access button to logout of Damus on the
side bar I didn't see a need for another logout button in the config
view.

Changelog-Changed: Remove redundant logout button from settings
Closes: #378
2023-01-30 16:01:22 -08:00
William Casarin
72f00fb413 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_fr_FR' 2023-01-30 15:56:57 -08:00
transifex-integration[bot]
d6694fac40 Apply translations in fr_FR
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-30 23:50:02 +00:00
William Casarin
d4068f8d52 Move Relay configuration to its own section on the sidebar
Changelog-Changed: Moved relay config to its own sidebar entry
2023-01-30 15:49:03 -08:00
7d410bff34 merge "Add LibreTranslate translations"
Changelog-Added: LibreTranslate note translations
2023-01-30 15:24:08 -08:00
ericholguin
b25e2ff6c0 Add custom picker component
Changelog-Changed: New stylized tabs
Closes: #391
2023-01-30 15:18:08 -08:00
eddff1a579 Allow profile edit button text to scale down when translation is too long
Closes: #432
2023-01-30 15:15:43 -08:00
387e1bcf22 Show EULA prior to login
Closes #413
2023-01-30 15:15:00 -08:00
4da002e1b4 Export translations
Closes: #439
2023-01-30 14:42:23 -08:00
William Casarin
139a2455a5 Merge branch 'translations' 2023-01-30 14:39:58 -08:00
William Casarin
e058f7e8e1 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_fr_FR' into translations 2023-01-30 14:39:36 -08:00
William Casarin
ec3f0b3c5d Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_pl_PL' into translations 2023-01-30 14:39:08 -08:00
William Casarin
20b1697e40 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_pt_PT' into translations 2023-01-30 14:38:24 -08:00
William Casarin
159f00e466 Merge remote-tracking branch 'github/translations_translations-en-us-xcloc-localized-contents-en-us-xliff--master_de' into translations 2023-01-30 14:38:12 -08:00
transifex-integration[bot]
57635b3c17 Apply translations in fr_FR
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-30 22:19:14 +00:00
900094fae4 Export source localization 2023-01-30 16:46:11 -05:00
4fbc9882ce Add LibreTranslate integration for machine translating notes from other languages 2023-01-30 16:46:06 -05:00
William Casarin
e1578c0337 Add support for account deletion
As per apple guidelines

Changelog-Added: Added support for account deletion
2023-01-30 13:26:04 -08:00
William Casarin
9fa11118d3 Fix some more context menu bugs 2023-01-30 12:43:24 -08:00
3aac4e2f7f Import pl_PL translations 2023-01-30 14:06:30 -05:00
133c237105 Import pt_PT translation 2023-01-30 14:05:32 -05:00
f59d267863 Import de translation 2023-01-30 14:04:34 -05:00
transifex-integration[bot]
78b4035d51 Apply translations in pl_PL
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pl_PL' language.
2023-01-30 18:48:40 +00:00
transifex-integration[bot]
dcc4b7b5e4 Apply translations in pt_PT
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-30 18:38:59 +00:00
transifex-integration[bot]
1af12e5e81 Apply translations in de
translation completed for the source file '/translations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de' language.
2023-01-30 18:38:56 +00:00
William Casarin
2eeeb081fd Merge branch 'translations' 2023-01-30 10:27:44 -08:00
William Casarin
7affc5ae4b Merge remote-tracking branch 'pl_PL' into translations 2023-01-30 10:27:23 -08:00
William Casarin
f283519a0d Merge remote-tracking branch 'de_AT' into translations 2023-01-30 10:27:14 -08:00
William Casarin
3317f23618 Merge remote-tracking branch 'master_de' into translations 2023-01-30 10:24:53 -08:00
2ed17a2509 Add export and import translation scripts
Closes: #430
2023-01-30 10:20:52 -08:00
08ca484d54 Fix height of DM input
Changelog-Fixed: Fixed height of DM input
Closes: #434
2023-01-30 10:19:47 -08:00
transifex-integration[bot]
2feaa207d7 Apply translations in pl_PL
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pl_PL' language.
2023-01-30 17:48:50 +00:00
bb6a09179e Fix bug with copying user pubkey
Co-authored-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Fixed bug where copying pubkey from context menu only copied your own pubkey
Closes: #425
2023-01-30 09:13:40 -08:00
49f64e7f49 Fix author attribution to the actual translators in CHANGELOG
Closes: #431
2023-01-30 08:59:28 -08:00
a65a6966ac Import de_AT localization 2023-01-29 11:43:07 -05:00
6f15746b8a Replace de-DE localization with de and import into project 2023-01-29 11:41:12 -05:00
transifex-integration[bot]
13066a8fa2 Apply translations in de_AT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-29 16:17:14 +00:00
transifex-integration[bot]
c647daf9b9 Apply translations in de
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de' language.
2023-01-29 16:07:13 +00:00
William Casarin
7bcc345038 Fix build 2023-01-28 15:54:52 -08:00
William Casarin
bf0f879d66 revert dubious change 2023-01-28 15:53:04 -08:00
William Casarin
3af9131afe autocomplete: add space after replacing occurances
This is more intuitive
2023-01-28 15:50:42 -08:00
Swift
b6b6d033a8 User tagging and autocompletion
Co-authored-by: William Casarin <jb55@jb55.com>
Changelog-Added: User tagging and autocompletion in posts
Closes: #347
Fixes: #411, #63
2023-01-28 15:43:45 -08:00
William Casarin
819d7496b2 v1.0.0-12 changelog 2023-01-28 10:54:49 -08:00
William Casarin
4c58e73e18 v1.0.0-12 2023-01-28 10:52:34 -08:00
William Casarin
6e38707aaa Merge remote-tracking branches 'ar' and 'pt_PT' 2023-01-28 09:55:02 -08:00
William Casarin
0f08612b79 Added arabic and portugese translations
Changelog-Added: Added arabic and portugese translations
2023-01-28 09:42:32 -08:00
transifex-integration[bot]
ef89c4b33b Apply translations in pt_PT
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:41:01 +00:00
transifex-integration[bot]
5c9bc02ac6 Apply translations in ar
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 17:40:50 +00:00
Joel Klabo
b57d2a3a6e Use AttributedString in NoteContentView
Changelog-Changed: Remove markdown link support from posts
Closes: #398
2023-01-28 09:38:38 -08:00
OlegAba
0e8c94b668 Replace SVGKit package with CoreSVG
Changelog-Fixed: Fixed crash on some SVG profile pictures
Closes: #416
2023-01-28 09:38:22 -08:00
transifex-integration[bot]
3e6c8c47a7 Apply translations in pt_PT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:00:11 +00:00
ericholguin
e4beb872a5 Add QRCode view
Changelog-Added: Add QRCode view for sharing your pubkey
Closes: #418
2023-01-28 08:36:53 -08:00
William Casarin
552bd9cae5 Implement NIP-21 URI handling
Changelog-Added: Added nostr: uri handling
2023-01-28 08:31:11 -08:00
transifex-integration[bot]
059a16a8dc Apply translations in ar
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 11:15:51 +00:00
William Casarin
b6ea17a0eb Merge remote-tracking branches 'tyiu/tyiu/string-comments' and 'tyiu/tyiu/remove-it-CH'
Changelog-Fixed: Localization fixes
Changelog-Fixed: Don't allow blocking yourself
2023-01-27 12:49:24 -08:00
William Casarin
a9e9f0dc8f Hide muted users from global
Changelog-Fixed: Hide muted users from global
2023-01-27 12:16:41 -08:00
William Casarin
5edb7df5c4 Mute events in threads
Changlog-Added: Mute events in threads
2023-01-27 12:11:57 -08:00
William Casarin
d559dd3a13 Allow non-string values in profiles
Profiles were not loading from other clients because some fields were
not strings

Changelog-Fixed: Fixed profiles sometimes not loading from other clients
2023-01-27 10:19:29 -08:00
William Casarin
b9c2473a2d reporting: don't use spam for every report
Changelog-Fixed: Fixed bug where `spam` was always the report type
2023-01-27 10:18:48 -08:00
William Casarin
196081cd38 mutelists: #d must be an array
Not sure how this was working before
2023-01-27 09:36:04 -08:00
3e02cc6889 Prevent blocking or reporting yourself 2023-01-27 00:56:32 -05:00
51f94cf135 Add missing comments to localized strings, reorder buttons, mark destructive buttons 2023-01-27 00:56:25 -05:00
a20fa08030 Remove it-CH locale as there is no difference with it-IT yet 2023-01-26 10:01:42 -05:00
William Casarin
203203a706 v1.0.0-11 changelog 2023-01-25 16:13:12 -08:00
William Casarin
92239eae69 v1.0.0-11 2023-01-25 16:12:19 -08:00
OlegAba
5de745fb19 Add double tap gesture and fix bugs
Changlog-Added: Image double-tap gesture
Closes: #397
2023-01-25 16:10:18 -08:00
Steven Briscoe
1baae90beb Fix to allow relays using ws://
Changelog-Fixed: allow ws:// relays again
2023-01-25 16:05:02 -08:00
Joel Klabo
2b832120ec Disable UI Tests 2023-01-25 16:04:15 -08:00
William Casarin
255668c17a Merge remote-tracking branch translation/ar 2023-01-25 16:03:29 -08:00
c046c7cf45 Add Reposts view
Changelog-Added: Reposts view
Closes: #376
2023-01-25 16:00:00 -08:00
William Casarin
5daaec35a8 Bump pfp/banner animated fize size limit to 5MiB/20MiB
Changelog-Changed: Bump pfp/banner animated fize size limit to 5MiB/20MiB
Closes: #384
2023-01-25 15:57:37 -08:00
William Casarin
abfbc8c9aa Merge branch 'translations'
Changelog-Added: Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV
2023-01-25 15:53:19 -08:00
William Casarin
44b1136b86 Merge remote-tracking branch 'it_IT' 2023-01-25 15:52:08 -08:00
William Casarin
dc28456122 Merge remote-tracking branch 'it_CH' 2023-01-25 15:50:59 -08:00
William Casarin
0dd804f61c Merge remote-tracking branches 'fr_FR', 'de_DE', 'de_AT' and 'lv_LV' 2023-01-25 15:49:12 -08:00
William Casarin
a3e7abc85d Update kieran's relay addr 2023-01-25 15:46:59 -08:00
Ricardo Arturo Cabral Mejía
d61d7df91b Remove wlvs.space relay, add eden.nostr.land
Phasing out nostr-relay.wlvs.space in favor of eden.nostr.land. Eden
relay is load balanced by geographical proximity and can handle more
clients.

Changelog-Changed: Updated default boostrap relays
2023-01-25 15:46:34 -08:00
William Casarin
5e3ce4e454 v1.0.0-10 changelog 2023-01-25 15:40:04 -08:00
William Casarin
59abc7b608 v1.0.0-10 2023-01-25 15:36:28 -08:00
William Casarin
74d8d57542 Add EULA step to account creation 2023-01-25 15:34:33 -08:00
William Casarin
214e45a98b Add muting and mutelists
- Filter muted posts from feed on mute
- List muted users in sidebar

Changelog-Added: Added ability to block users
2023-01-25 12:50:04 -08:00
William Casarin
2a8b9f75c1 Initial NIP-51 Mute List Implementation 2023-01-25 09:53:59 -08:00
William Casarin
7d323b65e4 Move share and report actions into a ... button 2023-01-25 08:41:05 -08:00
William Casarin
b69116e685 Move share button
To avoid the weird hit detection issues
2023-01-25 08:24:40 -08:00
William Casarin
561e2cd3ad refactor: add profile_button_style view extension 2023-01-25 08:24:35 -08:00
William Casarin
ad87a62486 [appstore] Report Content
This view provides a way to report content (nudity, illegal, spam) to
relays. Clients can use this information to filter or warn if they
choose to.

This is needed for the appstore release

Changelog-Added: Added a way to report content
2023-01-25 08:11:21 -08:00
transifex-integration[bot]
5793db4053 Apply translations in ar
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-25 08:03:22 +00:00
e736f8f837 Import localization for fr_FR 2023-01-24 21:35:12 -05:00
transifex-integration[bot]
81c1993156 Apply translations in fr_FR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-24 13:40:24 +00:00
4d97dbcacf Import localizations for it_CH 2023-01-23 16:01:32 -05:00
af72cf4e06 Import localization for it_IT 2023-01-23 16:00:57 -05:00
55ba3f8c1b Import localization for de_DE 2023-01-23 16:00:12 -05:00
d7ab33e731 Import localization for de-AT 2023-01-23 15:59:25 -05:00
1203b1d7fc Import localization for lv_LV 2023-01-23 15:58:07 -05:00
transifex-integration[bot]
a62f3e2737 Apply translations in lv_LV
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'lv_LV' language.
2023-01-23 20:47:32 +00:00
Swift
209a1c3213 Create stretchable profile cover header
Closes: #250
Changelog-Added: Stretchable profile cover header
2023-01-23 12:33:30 -08:00
William Casarin
675903b768 SelectedEventView: use EventProfile 2023-01-23 12:30:29 -08:00
William Casarin
92035e17d3 refactor: move reply_desc to ReplyDescription 2023-01-23 12:20:02 -08:00
William Casarin
8df5bf04ae refactor: Break EventView into 3 separate views
SelectedEventView
EmbeddedEventView
EventView
2023-01-23 12:13:58 -08:00
transifex-integration[bot]
cf79fd9491 Apply translations in de_AT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-23 19:27:44 +00:00
transifex-integration[bot]
56d43f1ad1 Apply translations in de_DE
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-23 19:27:40 +00:00
William Casarin
7e1daf7816 refactor: move Highlight to its own file 2023-01-23 10:58:39 -08:00
William Casarin
0ead583bda refactor: move BuilderEventView to it's own file 2023-01-23 10:58:39 -08:00
William Casarin
dd44bd779b EventView: hide extra previews
Getting too busy
2023-01-23 10:58:39 -08:00
William Casarin
c31374fc0a build 9 2023-01-23 10:28:36 -08:00
transifex-integration[bot]
984a1b916d Apply translations in it_CH
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'it_CH' language.
2023-01-23 15:36:11 +00:00
transifex-integration[bot]
b8cefb9392 Apply translations in it_IT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'it_IT' language.
2023-01-23 15:26:15 +00:00
William Casarin
f3730630b5 Merge tyiu/string-fixes, transifex/de_AT, transifex/de_DE
More translation stuff from Terry
2023-01-22 19:36:51 -08:00
d5f2a17249 Import localization for de-AT 2023-01-22 21:57:47 -05:00
4526ed01fe Import localization for de-DE 2023-01-22 21:56:35 -05:00
transifex-integration[bot]
a590fb099d Apply translations in de_AT
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-23 02:54:31 +00:00
transifex-integration[bot]
135814737c Apply translations in de_DE
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-23 02:54:20 +00:00
e2e60639d9 Remove unused language mapping from transifex.yml 2023-01-22 21:53:30 -05:00
83909c8fc9 Change use of DM to be consistent and add missing localization comments 2023-01-22 21:50:28 -05:00
William Casarin
3e4914462b EventDetailBar disappeared. Let's fix that before (8) 2023-01-22 10:37:33 -08:00
William Casarin
7a11433a98 v1.0.0-8 changelog 2023-01-22 10:28:51 -08:00
William Casarin
03e1c1903f v1.0.0-8 2023-01-22 10:27:35 -08:00
William Casarin
acdee6a326 Show Website on profiles
Changelog-Added: Show website on profiles
2023-01-22 10:25:12 -08:00
William Casarin
f5e03f145c Fix build 2023-01-22 10:10:54 -08:00
Joel Klabo
2a9ddd10c8 Choose Participants on a Thread Reply
Closes: #345
Changelog-Added: Add the ability to choose participants when replying
2023-01-22 10:10:13 -08:00
5e9580377d Fix localized string for privacy access description for photos
Closes: #359
2023-01-22 10:00:09 -08:00
William Casarin
9d2ff2fe65 Fix commas and emojis getting included in hashtags
Changelog-Fixed: Fix commands and emojis getting included in hashtags
2023-01-22 09:57:00 -08:00
Joel Klabo
13ea42a2e2 Don't Parse URL with Only Whitespace 2023-01-22 09:49:41 -08:00
William Casarin
d9e22ce7bf Add translations for de_AT, de_DE, tr_TR, fr_FR
Changelog-Added: Translations for de_AT, de_DE, tr_TR, fr_FR
2023-01-22 09:43:51 -08:00
William Casarin
2335a65b78 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_fr_FR' 2023-01-22 09:42:57 -08:00
William Casarin
566cd141ce Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_tr_TR' 2023-01-22 09:42:06 -08:00
William Casarin
55f7f8c072 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_de_DE' 2023-01-22 09:40:46 -08:00
William Casarin
4b4addd215 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_de_AT' 2023-01-22 09:39:51 -08:00
Joel Klabo
255a0c55ba Update CI Specify Xcode Version
Closes: #360
2023-01-22 09:26:29 -08:00
Thomas Rademaker
ced6e2488f Fix duplicate post buttons when swiping tabs
Move the postButtonContainer view to avoid having 2 buttons on screen when swiping tabs

Closes: #366
Changelog-Fixed: Fix duplicate post buttons when swiping tabs
2023-01-22 09:25:16 -08:00
Thomas Rademaker
77c2abc524 fix broken test 2023-01-22 09:25:16 -08:00
Joel Klabo
e4ad15ced1 Center PFP in Zoom View
Closes: #357
Changlog-Fixed: Center profile picture in zoom view
2023-01-22 09:23:11 -08:00
Joel Klabo
904a6e960a Cleanup X Padding and Size 2023-01-22 09:23:11 -08:00
da8a82954a Import localization for fr-FR 2023-01-21 20:59:57 -05:00
383f45fe96 Import localization for de-DE 2023-01-21 20:58:21 -05:00
9dc0f3baf6 Import localization for de-AT 2023-01-21 20:57:34 -05:00
abc857582f Import localization for tr-TR 2023-01-21 20:54:33 -05:00
transifex-integration[bot]
4816b57dcd Apply translations in de_AT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-21 23:34:52 +00:00
transifex-integration[bot]
06b1953b49 Apply translations in de_DE
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-21 23:33:33 +00:00
William Casarin
d658d1d987 DM Message Requests
Put strangers in a different tab

Changelog-Added: Add DM Message Requests
2023-01-21 11:13:44 -08:00
William Casarin
e14cd99c85 Add first_eref_mention helper to refactor embedded builder 2023-01-21 10:29:40 -08:00
William Casarin
0258ef792f Show embedded note references
This reverts commit 3f3b78f9bc.

Changelog-Fixed: Show embedded note references
2023-01-21 09:57:29 -08:00
transifex-integration[bot]
52524e00a2 Apply translations in tr_TR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'tr_TR' language.
2023-01-21 09:10:29 +00:00
transifex-integration[bot]
342883fbb0 Apply translations in fr_FR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-20 22:00:04 +00:00
William Casarin
1e6505abe3 v1.0.0-7 changelog 2023-01-20 09:35:51 -08:00
OlegAba
98c24147e8 Drastically improve image viewer
Changelog-Added: Drastically improved image viewer
Closes: #349
2023-01-20 09:33:22 -08:00
Joel Klabo
d1e7de5dcb Add Preview with 999 Everything 2023-01-20 09:23:42 -08:00
Joel Klabo
11e0a87f06 Fix ... when too many likes/reposts
Closes: #351
Changelog-Fixed: Fix ... when too many likes/reposts
2023-01-20 09:23:14 -08:00
Swift
1154cec719 Avoid showing reboost alert for pubkey user
Closes: #337
Changelog-Fixed: Don't show report alert if logged in as a pubkey
2023-01-19 10:10:07 -08:00
Joel Klabo
1ecfb0487e Swipe to Dismiss Images
Closes: #343
2023-01-18 13:36:57 -08:00
Swift
8ffa8446b6 Image Pinch Zooming
Changelog-Added: Added pinch to zoom on images
2023-01-18 10:10:22 -08:00
Ben Weeks
fb1f99e728 Fixed Jack's issue with homepage gap at the top.
Closes: #328
Changelog-Fixed: Fix padding issue at top of home timeline
2023-01-18 09:59:26 -08:00
Zach Hendel
031408dec3 Makes both name and @username clickable to go to profile
Changelog-Changed: Makes both name and username clickable in sidebar to go to profile
Closes: #329
2023-01-18 09:54:18 -08:00
Zach Hendel
16b6d029fa Icons for hardclick menu event view
changes copy user id icon to person
changes copy note id icon to note.text
changes copy note json icon to magnifying glass

Closes: #330
2023-01-18 09:48:58 -08:00
Brandon Holm
3e093e8572 Added Blixt Wallet TestFlight link
Closes: #331
2023-01-18 09:48:49 -08:00
John Bethancourt
fa11af4b1d Prevent absurdly large sidebar on Mac or iPad
Closes: #338
Changelog-Fixed: Fix absurdly large sidebar on Mac/iPad
2023-01-18 09:39:35 -08:00
William Casarin
91159d70ca Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_es_419'
Changelog-Added: Add Latin American Spanish translations
2023-01-18 09:30:53 -08:00
OlegAba
a57d654f32 Fix tab views moving after selecting from search result
Closes: #339
Changelog-Fixed: Fix tab views moving after selecting from search result
2023-01-18 09:29:30 -08:00
OlegAba
0313480685 Change navigation title to bold
Closes: #341
2023-01-18 09:29:04 -08:00
OlegAba
e07b31e0a1 Consistent follow/unfollow button width
Changelog-Fixed: Make follow/unfollow button a consistent width
2023-01-18 09:28:29 -08:00
538a0ae5ea Import es-419 localization files 2023-01-16 20:44:41 -05:00
0f82db2440 Delete unused exported es-419 localization 2023-01-16 20:44:11 -05:00
transifex-integration[bot]
92ae2c7754 Apply translations in es_419
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'es_419' language.
2023-01-16 16:59:06 +00:00
William Casarin
00c819140b Don't include garbage in notifications
Check to make sure we have our pubkey on events coming into
notifications. Sometimes relays are buggy.

Changelog-Fixed: Don't add events to notifications from buggy relays
2023-01-15 12:53:37 -08:00
OlegAba
cbc3c46c9d Fix image crash and support SVG profile pictures
Closes: #310
Changelog-Fixed: Fixed some crashes with large images
Changelog-Added: Added SVG profile picture support
2023-01-14 19:30:54 -08:00
radixrat
4b5c34b4e2 Click pfp in side menu should open profile as well
Closes: #323
Changelog-Changed: Clicking pfp in sidebar opens profile as well
2023-01-14 18:49:36 -08:00
ericholguin
9eb39f7e0a Don't blur images if your friend boosted it
Closes: #322
Changelog-Changed: Don't blur images if your friend boosted it
2023-01-14 18:47:45 -08:00
173b22b772 localization: fix string bugs
Closes: #321
2023-01-14 18:44:11 -08:00
William Casarin
18c7cba53c Minor refactor 2023-01-14 17:24:03 -08:00
William Casarin
9a40fd595d Add some DM sorting tests
They didn't help me fix the problem, but maybe they are still useful
somehow
2023-01-14 17:23:35 -08:00
William Casarin
a71c35a6b0 Fix DM sorting bug
Changelog-Fixed: Fix DM sorting on incoming messages
2023-01-14 17:21:44 -08:00
William Casarin
d69d3cc74e create_dm: allow created_at argument
This is mainly used by tests
2023-01-14 16:28:18 -08:00
William Casarin
6e220ac4c1 dms: extract incoming DM handling into pure functions
So that it will be easier to test
2023-01-14 09:48:31 -08:00
William Casarin
bcb40a6ec7 Fix text getting truncated next to link previews
Changelog-Fixed: Fix text getting truncated next to link previews
2023-01-14 09:30:22 -08:00
Swift
2f1063b49f Resolve issue on Roboash not appearing in sidebar pfp
Also works fine with users with profile pic url set

Closes: #315
2023-01-14 09:00:59 -08:00
Swift
73110952e5 Avoid showing wrong alert message for pubkey login user. 2023-01-14 08:58:27 -08:00
d01e7c0595 Fix localization bugs and export localizations
Changelog-Fixes: Fix some localization bugs
Closes: #320
2023-01-14 08:54:43 -08:00
Ben Weeks
aba758b143 Fixes damus-io/damus#246 ChangeLog-Changed: Resolved issue with image URLs in uppercase (@npub1jutptdc2m8kgjmudtws095qk2tcale0eemvp4j2xnjnl4nh6669slrf04x) 2023-01-06 00:39:57 +00:00
Ben Weeks
b7c7b0b3bf Merge branch 'damus-io:master' into beautify-image-zoom 2023-01-06 00:25:15 +00:00
Ben Weeks
2d10d4592b Fixes damus-io/damus#254 Changes background to Color("DamusDarkGrey") ChangeLog-Changed: Allowed pinch and zoom on profile picture (scoder1747) ChangeLog-Changed: Allowed pinch and zoom to post pictures (@npub1jutptdc2m8kgjmudtws095qk2tcale0eemvp4j2xnjnl4nh6669slrf04x) 2023-01-06 00:16:48 +00:00
300 changed files with 19511 additions and 4702 deletions

View File

@@ -1,4 +1,6 @@
name: Test
name: Run Test Suite
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
on:
push:
branches:
@@ -6,12 +8,24 @@ on:
pull_request:
branches:
- "*"
jobs:
test:
name: Run Tests
runs-on: macos-latest
run_tests:
runs-on: macos-12
strategy:
matrix:
include:
- xcode: "14.2"
ios: "16.2"
name: Test iOS (${{ matrix.ios }})
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Running Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' | xcpretty && exit ${PIPESTATUS[0]}
- name: Checkout
uses: actions/checkout@v1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Run Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=${{ matrix.ios }}' | xcpretty && exit ${PIPESTATUS[0]}

View File

@@ -1,3 +1,230 @@
## [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
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
### Changed
- Remove redundant logout button from settings (Jonathan Milligan)
- Moved relay config to its own sidebar entry (William Casarin)
- New stylized tabs (ericholguin)
### Fixed
- Fix hidden profile action sheet when clicking ... (William Casarin)
- Fixed height of DM input (Terry Yiu)
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
## [1.0.0-12] - 2023-01-28
### Added
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
### Changed
- Remove markdown link support from posts (Joel Klabo)
### Fixed
- Fixed crash on some SVG profile pictures (OlegAba)
- Localization fixes
- Don't allow blocking yourself (Terry)
- Hide muted users from global (William Casarin)
- Fixed profiles sometimes not loading from other clients (William Casarin)
- Fixed bug where `spam` was always the report type (William Casarin)
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
## [1.0.0-11] - 2023-01-25
### Added
- Reposts view (Terry Yiu)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
### Changed
- Bump pfp/banner animated fize size limit to 5MiB/20MiB (William Casarin)
- Updated default boostrap relays (Ricardo Arturo Cabral Mejía)
### Fixed
- allow ws:// relays again (Steven Briscoe)
[1.0.0-11]: https://github.com/damus-io/damus/releases/tag/v1.0.0-11
## [1.0.0-8] - 2023-01-22
### Added
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Add DM Message Requests (William Casarin)
### Fixed
- Fix commands and emojis getting included in hashtags (William Casarin)
- Fix duplicate post buttons when swiping tabs (Thomas Rademaker)
- Show embedded note references (William Casarin)
[1.0.0-8]: https://github.com/damus-io/damus/releases/tag/v1.0.0-8
## [1.0.0-7] - 2023-01-20
### Added
- Drastically improved image viewer (OlegAba)
- Added pinch to zoom on images (Swift)
- Add Latin American Spanish translations (Nicolás Valencia)
- Added SVG profile picture support (OlegAba)
### Changed
- Makes both name and username clickable in sidebar to go to profile (Zach Hendel)
- Clicking pfp in sidebar opens profile as well (radixrat)
- Don't blur images if your friend boosted it (ericholguin)
### Fixed
- Fix ... when too many likes/reposts (Joel Klabo)
- Don't show report alert if logged in as a pubkey (Swift)
- Fix padding issue at top of home timeline (Ben Weeks)
- Fix absurdly large sidebar on Mac/iPad (John Bethancourt)
- Fix tab views moving after selecting from search result (OlegAba)
- Make follow/unfollow button a consistent width (OlegAba)
- Don't add events to notifications from buggy relays (William Casarin)
- Fixed some crashes with large images (OlegAba)
- Fix DM sorting on incoming messages (William Casarin)
- Fix text getting truncated next to link previews (William Casarin)
[1.0.0-7]: https://github.com/damus-io/damus/releases/tag/v1.0.0-7
## [1.0.0-6] - 2023-01-13
### Added
@@ -384,4 +611,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2

View File

@@ -1,3 +1,4 @@
[![Run Test Suite](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml/badge.svg?branch=master)](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
# damus
@@ -25,7 +26,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Getting Started on Damus
### Damus iOS
1) Get the Damus app on TestFlight: https://testflight.apple.com/join/CLwjLxWl
1) Get the Damus app on the iOS App Store: https://apps.apple.com/ca/app/damus/id1628663131
#### ⚙️ Settings (gear icon, top right)
- Relays: You can add more relays to send your notes to by tapping the "+".
@@ -48,7 +49,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
- Currently you can't delete your Notes in the iOS app
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `https://i.ibb.co/2SHZbwm/alpha60.jpg`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Engaging with Notes
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users notifications and in your 🏠 Personal and 🔍 Global Feeds
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
@@ -91,15 +92,23 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Contributing
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
Contributors welcome!
### Code
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
[git-send-email]: http://git-send-email.io
## git log bot
### Translations
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
Translators welcome! Join the [Transifex][transifex] project.
### Awards
All user-facing strings must have a comment in order to provide context to translators. If a SwiftUI component has a `comment` parameter, use that. Otherwise, wrap your string with `NSLocalizedString` with the `comment` field populated.
[transifex]: https://explore.transifex.com/damus/damus-ios/
### Awards
There may be nostr badges awarded for contributors in the future... :)
@@ -107,3 +116,7 @@ First contributors:
1. @randymcmillan
2. @jcarucci27
### git log bot
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas

View File

@@ -1,905 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
<source>Damus</source>
<target>Damus</target>
<note>Bundle display name</note>
</trans-unit>
<trans-unit id="CFBundleName" xml:space="preserve">
<source>damus</source>
<target>damus</target>
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>"Granting Damus access to your photo library allows you to save photos.</source>
<target>"Granting Damus access to your photo library allows you to save photos.</target>
<note>Privacy - Photo Library Additions Usage Description</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target> </target>
<note>Blank space to separate profile picture from profile editor form.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<target>%@</target>
<note>Number of people following a user.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target>%@ %@</target>
<note>Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.</note>
</trans-unit>
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
<target>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</target>
<note>Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" xml:space="preserve">
<source>%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs</source>
<target>%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs</target>
<note>Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet." xml:space="preserve">
<source>%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.</source>
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.</target>
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%lld" xml:space="preserve">
<source>%lld</source>
<target>%lld</target>
<note>Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
<source>%lld/%lld</source>
<target>%lld/%lld</target>
<note>Fraction of how many of the user's relay servers that are operational.</note>
</trans-unit>
<trans-unit id="&amp;nbsp;" xml:space="preserve">
<source>&amp;nbsp;</source>
<target>&amp;nbsp;</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="'%@' at '%@' will be used for verification" xml:space="preserve">
<source>'%@' at '%@' will be used for verification</source>
<target>'%@' at '%@' will be used for verification</target>
<note>Description of how the nip05 identifier would be used for verification.</note>
</trans-unit>
<trans-unit id="'%@' is an invalid nip05 identifier. It should look like an email." xml:space="preserve">
<source>'%@' is an invalid nip05 identifier. It should look like an email.</source>
<target>'%@' is an invalid nip05 identifier. It should look like an email.</target>
<note>Description of why the nip05 identifier is invalid.</note>
</trans-unit>
<trans-unit id="(Profile.displayName(profile: profile, pubkey: whos))'s Followers" xml:space="preserve">
<source>(Profile.displayName(profile: profile, pubkey: whos))'s Followers</source>
<target>(Profile.displayName(profile: profile, pubkey: whos))'s Followers</target>
<note>Navigation bar title for view that shows who is following a user.</note>
</trans-unit>
<trans-unit id="(formattedSats) sat" xml:space="preserve">
<source>(formattedSats) sat</source>
<target>(formattedSats) sat</target>
<note>Amount of 1 sat.</note>
</trans-unit>
<trans-unit id="(formattedSats) sats" xml:space="preserve">
<source>(formattedSats) sats</source>
<target>(formattedSats) sats</target>
<note>Amount of sats.</note>
</trans-unit>
<trans-unit id="(who) following" xml:space="preserve">
<source>(who) following</source>
<target>(who) following</target>
<note>Navigation bar title for view that shows who a user is following.</note>
</trans-unit>
<trans-unit id="&lt; e &gt;" xml:space="preserve">
<source>&lt; e &gt;</source>
<target>&lt; e &gt;</target>
<note>Placeholder for event mention.</note>
</trans-unit>
<trans-unit id="@" xml:space="preserve">
<source>@</source>
<target>@</target>
<note>Prefix character to username.</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
<target>About</target>
<note>Label to prompt for about text entry for user to describe about themself.</note>
</trans-unit>
<trans-unit id="About Me" xml:space="preserve">
<source>About Me</source>
<target>About Me</target>
<note>Label for About Me section of user profile form.</note>
</trans-unit>
<trans-unit id="Absolute Boss" xml:space="preserve">
<source>Absolute Boss</source>
<target>Absolute Boss</target>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Account ID" xml:space="preserve">
<source>Account ID</source>
<target>Account ID</target>
<note>Label to indicate the public ID of the account.</note>
</trans-unit>
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<target>Add</target>
<note>Button to add recommended relay server.
Button to confirm adding user inputted relay.</note>
</trans-unit>
<trans-unit id="Add Relay" xml:space="preserve">
<source>Add Relay</source>
<target>Add Relay</target>
<note>Label for section for adding a relay server.</note>
</trans-unit>
<trans-unit id="Any" xml:space="preserve">
<source>Any</source>
<target>Any</target>
<note>Any amount of sats</note>
</trans-unit>
<trans-unit id="Are you sure you want to boost this post?" xml:space="preserve">
<source>Are you sure you want to boost this post?</source>
<target>Are you sure you want to boost this post?</target>
<note>Alert message to ask if user wants to boost a post.</note>
</trans-unit>
<trans-unit id="Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." xml:space="preserve">
<source>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</source>
<target>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</target>
<note>Reminder to user that they should save their account information.</note>
</trans-unit>
<trans-unit id="Bitcoin Beach" xml:space="preserve">
<source>Bitcoin Beach</source>
<target>Bitcoin Beach</target>
<note>Dropdown option label for Lightning wallet, Bitcoin Beach.</note>
</trans-unit>
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
<source>Bitcoin Lightning Tips</source>
<target>Bitcoin Lightning Tips</target>
<note>Label for Bitcoin Lightning Tips section of user profile form.</note>
</trans-unit>
<trans-unit id="Blixt Wallet" xml:space="preserve">
<source>Blixt Wallet</source>
<target>Blixt Wallet</target>
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
</trans-unit>
<trans-unit id="Blue Wallet" xml:space="preserve">
<source>Blue Wallet</source>
<target>Blue Wallet</target>
<note>Dropdown option label for Lightning wallet, Blue Wallet.</note>
</trans-unit>
<trans-unit id="Boost" xml:space="preserve">
<source>Boost</source>
<target>Boost</target>
<note>Button to confirm boosting a post.
Title of alert for confirming to boost a post.</note>
</trans-unit>
<trans-unit id="Boosted" xml:space="preserve">
<source>Boosted</source>
<target>Boosted</target>
<note>Text indicating that the post was boosted (i.e. re-shared).</note>
</trans-unit>
<trans-unit id="Breez" xml:space="preserve">
<source>Breez</source>
<target>Breez</target>
<note>Dropdown option label for Lightning wallet, Breez.</note>
</trans-unit>
<trans-unit id="Broadcast" xml:space="preserve">
<source>Broadcast</source>
<target>Broadcast</target>
<note>Context menu option for broadcasting the user's note to all of the user's connected relay servers.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
<note>Button to cancel out of posting a note.
Button to cancel out of view adding user inputted relay.
Cancel out of logging out the user.</note>
</trans-unit>
<trans-unit id="Cash App" xml:space="preserve">
<source>Cash App</source>
<target>Cash App</target>
<note>Dropdown option label for Lightning wallet, Cash App.</note>
</trans-unit>
<trans-unit id="Chat" xml:space="preserve">
<source>Chat</source>
<target>Chat</target>
<note>Navigation bar title for Chatroom view.</note>
</trans-unit>
<trans-unit id="Clear" xml:space="preserve">
<source>Clear</source>
<target>Clear</target>
<note>Button for clearing cached data.</note>
</trans-unit>
<trans-unit id="Clear Cache" xml:space="preserve">
<source>Clear Cache</source>
<target>Clear Cache</target>
<note>Section title for clearing cached data.</note>
</trans-unit>
<trans-unit id="Copied" xml:space="preserve">
<source>Copied</source>
<target>Copied</target>
<note>Label indicating that a user's key was copied.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
<source>Copy</source>
<target>Copy</target>
<note>Button to copy a relay server address.</note>
</trans-unit>
<trans-unit id="Copy Account ID" xml:space="preserve">
<source>Copy Account ID</source>
<target>Copy Account ID</target>
<note>Context menu option for copying the ID of the account that created the note.</note>
</trans-unit>
<trans-unit id="Copy Image" xml:space="preserve">
<source>Copy Image</source>
<target>Copy Image</target>
<note>Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard.</note>
</trans-unit>
<trans-unit id="Copy Image URL" xml:space="preserve">
<source>Copy Image URL</source>
<target>Copy Image URL</target>
<note>Context menu option to copy the URL of an image into clipboard.</note>
</trans-unit>
<trans-unit id="Copy LNURL" xml:space="preserve">
<source>Copy LNURL</source>
<target>Copy LNURL</target>
<note>Context menu option for copying a user's Lightning URL.</note>
</trans-unit>
<trans-unit id="Copy Note ID" xml:space="preserve">
<source>Copy Note ID</source>
<target>Copy Note ID</target>
<note>Context menu option for copying the ID of the note.</note>
</trans-unit>
<trans-unit id="Copy Note JSON" xml:space="preserve">
<source>Copy Note JSON</source>
<target>Copy Note JSON</target>
<note>Context menu option for copying the JSON text from the note.</note>
</trans-unit>
<trans-unit id="Copy Text" xml:space="preserve">
<source>Copy Text</source>
<target>Copy Text</target>
<note>Context menu option for copying the text from an note.</note>
</trans-unit>
<trans-unit id="Copy User ID" xml:space="preserve">
<source>Copy User ID</source>
<target>Copy User ID</target>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
<trans-unit id="Copy invoice" xml:space="preserve">
<source>Copy invoice</source>
<target>Copy invoice</target>
<note>Title of section for copying a Lightning invoice identifier.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve">
<source>Create</source>
<target>Create</target>
<note>Button to create account.</note>
</trans-unit>
<trans-unit id="Create Account" xml:space="preserve">
<source>Create Account</source>
<target>Create Account</target>
<note>Button to create an account.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<target>Creator(s) of Bitcoin. Absolute legend.</target>
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="DM" xml:space="preserve">
<source>DM</source>
<target>DM</target>
<note>Navigation title for DM view, which is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Damus" xml:space="preserve">
<source>Damus</source>
<target>Damus</target>
<note>Name of the app, shown on the first screen when user is not logged in.</note>
</trans-unit>
<trans-unit id="Default Wallet" xml:space="preserve">
<source>Default Wallet</source>
<target>Default Wallet</target>
<note>Button to pay a Lightning invoice with the user's default Lightning wallet.</note>
</trans-unit>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
<target>Delete</target>
<note>Button to delete a relay server that the user connects to.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
<target>Dismiss</target>
<note>Button to dismiss a text field alert.</note>
</trans-unit>
<trans-unit id="Display Name" xml:space="preserve">
<source>Display Name</source>
<target>Display Name</target>
<note>Label to prompt display name entry.</note>
</trans-unit>
<trans-unit id="Done" xml:space="preserve">
<source>Done</source>
<target>Done</target>
<note>Button to dismiss wallet selection view for paying Lightning invoice.</note>
</trans-unit>
<trans-unit id="Earn Money" xml:space="preserve">
<source>Earn Money</source>
<target>Earn Money</target>
<note>Heading indicating that this application allows users to earn money.</note>
</trans-unit>
<trans-unit id="Edit" xml:space="preserve">
<source>Edit</source>
<target>Edit</target>
<note>Button to edit user's profile.</note>
</trans-unit>
<trans-unit id="Edit Profile" xml:space="preserve">
<source>Edit Profile</source>
<target>Edit Profile</target>
<note>Title of navigation view for Edit Profile.</note>
</trans-unit>
<trans-unit id="Encrypted" xml:space="preserve">
<source>Encrypted</source>
<target>Encrypted</target>
<note>Heading indicating that this application keeps private messaging end-to-end encrypted.</note>
</trans-unit>
<trans-unit id="Encrypted DMs" xml:space="preserve">
<source>Encrypted DMs</source>
<target>Encrypted DMs</target>
<note>Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Enter your account key to login:" xml:space="preserve">
<source>Enter your account key to login:</source>
<target>Enter your account key to login:</target>
<note>Prompt for user to enter an account key to login.</note>
</trans-unit>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Error: %@</target>
<note>Error message indicating why saving keys failed.</note>
</trans-unit>
<trans-unit id="Filter State" xml:space="preserve">
<source>Filter State</source>
<target>Filter State</target>
<note>Filter state for seeing either only posts, or posts &amp; replies.</note>
</trans-unit>
<trans-unit id="Follow" xml:space="preserve">
<source>Follow</source>
<target>Follow</target>
<note>Button to follow a user.</note>
</trans-unit>
<trans-unit id="Followers" xml:space="preserve">
<source>Followers</source>
<target>Followers</target>
<note>Label describing followers of a user.</note>
</trans-unit>
<trans-unit id="Following" xml:space="preserve">
<source>Following</source>
<target>Following</target>
<note>Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following.</note>
</trans-unit>
<trans-unit id="Following..." xml:space="preserve">
<source>Following...</source>
<target>Following...</target>
<note>Label to indicate that the user is in the process of following another user.</note>
</trans-unit>
<trans-unit id="Follows" xml:space="preserve">
<source>Follows</source>
<target>Follows</target>
<note>Text to indicate that button next to it is in a state that will follow a profile when tapped.</note>
</trans-unit>
<trans-unit id="Global" xml:space="preserve">
<source>Global</source>
<target>Global</target>
<note>Navigation bar title for Global view where posts from all connected relay servers appear.</note>
</trans-unit>
<trans-unit id="Goto post %@" xml:space="preserve">
<source>Goto post %@</source>
<target>Goto post %@</target>
<note>Navigation link to go to post referenced by hex code.</note>
</trans-unit>
<trans-unit id="Goto profile %@" xml:space="preserve">
<source>Goto profile %@</source>
<target>Goto profile %@</target>
<note>Navigation link to go to profile.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<target>Home</target>
<note>Navigation bar title for Home view where posts and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Invalid key" xml:space="preserve">
<source>Invalid key</source>
<target>Invalid key</target>
<note>Error message indicating that an invalid account key was entered for login.</note>
</trans-unit>
<trans-unit id="LNLink" xml:space="preserve">
<source>LNLink</source>
<target>LNLink</target>
<note>Dropdown option label for Lightning wallet, LNLink.</note>
</trans-unit>
<trans-unit id="Let's go!" xml:space="preserve">
<source>Let's go!</source>
<target>Let's go!</target>
<note>Button to complete account creation and start using the app.</note>
</trans-unit>
<trans-unit id="Lightning Address or LNURL" xml:space="preserve">
<source>Lightning Address or LNURL</source>
<target>Lightning Address or LNURL</target>
<note>Placeholder text for entry of Lightning Address or LNURL.</note>
</trans-unit>
<trans-unit id="Lightning Invoice" xml:space="preserve">
<source>Lightning Invoice</source>
<target>Lightning Invoice</target>
<note>Indicates that the view is for paying a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Local default" xml:space="preserve">
<source>Local default</source>
<target>Local default</target>
<note>Dropdown option label for system default for Lightning wallet.</note>
</trans-unit>
<trans-unit id="Login" xml:space="preserve">
<source>Login</source>
<target>Login</target>
<note>Button to log into account.</note>
</trans-unit>
<trans-unit id="Logout" xml:space="preserve">
<source>Logout</source>
<target>Logout</target>
<note>Alert for logging out the user.
Button for logging out the user.
Button to logout the user.</note>
</trans-unit>
<trans-unit id="Make sure your nsec account key is saved before you logout or you will lose access to this account" xml:space="preserve">
<source>Make sure your nsec account key is saved before you logout or you will lose access to this account</source>
<target>Make sure your nsec account key is saved before you logout or you will lose access to this account</target>
<note>Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.</note>
</trans-unit>
<trans-unit id="Muun" xml:space="preserve">
<source>Muun</source>
<target>Muun</target>
<note>Dropdown option label for Lightning wallet, Muun.</note>
</trans-unit>
<trans-unit id="NIP-05 Verification" xml:space="preserve">
<source>NIP-05 Verification</source>
<target>NIP-05 Verification</target>
<note>Label for NIP-05 Verification section of user profile form.</note>
</trans-unit>
<trans-unit id="Nothing to see here. Check back later!" xml:space="preserve">
<source>Nothing to see here. Check back later!</source>
<target>Nothing to see here. Check back later!</target>
<note>Indicates that there are no notes in the timeline to view.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifications</target>
<note>Navigation title for notifications.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
<target>Pay</target>
<note>Button to pay a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Pay the Lightning invoice" xml:space="preserve">
<source>Pay the Lightning invoice</source>
<target>Pay the Lightning invoice</target>
<note>Navigation bar title for view to pay Lightning invoice.</note>
</trans-unit>
<trans-unit id="Phoenix" xml:space="preserve">
<source>Phoenix</source>
<target>Phoenix</target>
<note>Dropdown option label for Lightning wallet, Phoenix.</note>
</trans-unit>
<trans-unit id="Post" xml:space="preserve">
<source>Post</source>
<target>Post</target>
<note>Button to post a note.</note>
</trans-unit>
<trans-unit id="Posts" xml:space="preserve">
<source>Posts</source>
<target>Posts</target>
<note>Label for filter for seeing only posts (instead of posts and replies).</note>
</trans-unit>
<trans-unit id="Posts &amp; Replies" xml:space="preserve">
<source>Posts &amp; Replies</source>
<target>Posts &amp; Replies</target>
<note>Label for filter for seeing posts and replies (instead of only posts).</note>
</trans-unit>
<trans-unit id="Private" xml:space="preserve">
<source>Private</source>
<target>Private</target>
<note>Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.</note>
</trans-unit>
<trans-unit id="Private Key" xml:space="preserve">
<source>Private Key</source>
<target>Private Key</target>
<note>Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.</note>
</trans-unit>
<trans-unit id="PrivateKey" xml:space="preserve">
<source>PrivateKey</source>
<target>PrivateKey</target>
<note>Title of the secure field that holds the user's private key.</note>
</trans-unit>
<trans-unit id="Profile Picture" xml:space="preserve">
<source>Profile Picture</source>
<target>Profile Picture</target>
<note>Label for Profile Picture section of user profile form.</note>
</trans-unit>
<trans-unit id="Public Account ID" xml:space="preserve">
<source>Public Account ID</source>
<target>Public Account ID</target>
<note>Section title for the user's public account ID.</note>
</trans-unit>
<trans-unit id="Public Key" xml:space="preserve">
<source>Public Key</source>
<target>Public Key</target>
<note>Label indicating that the text is a user's public account key.</note>
</trans-unit>
<trans-unit id="Public Key?" xml:space="preserve">
<source>Public Key?</source>
<target>Public Key?</target>
<note>Prompt to ask user if the key they entered is a public key.</note>
</trans-unit>
<trans-unit id="Public key" xml:space="preserve">
<source>Public key</source>
<target>Public key</target>
<note>Label indicating that the text is a user's public account key.</note>
</trans-unit>
<trans-unit id="Recommended Relays" xml:space="preserve">
<source>Recommended Relays</source>
<target>Recommended Relays</target>
<note>Section title for recommend relay servers that could be added as part of configuration</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>Relay</target>
<note>Text field for relay server. Used for testing purposes.</note>
</trans-unit>
<trans-unit id="Relays" xml:space="preserve">
<source>Relays</source>
<target>Relays</target>
<note>Header text for relay server list for configuration.
Part of a larger sentence to describe how many relay servers a user is connected.</note>
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<target>Reply to self</target>
<note>Label to indicate that the user is replying to themself.</note>
</trans-unit>
<trans-unit id="Replying to %@ &amp; %@" xml:space="preserve">
<source>Replying to %1$@ &amp; %2$@</source>
<target>Replying to %1$@ &amp; %2$@</target>
<note>Label to indicate that the user is replying to 2 users.</note>
</trans-unit>
<trans-unit id="Replying to:" xml:space="preserve">
<source>Replying to:</source>
<target>Replying to:</target>
<note>Indicating that the user is replying to the following listed people.</note>
</trans-unit>
<trans-unit id="Reset" xml:space="preserve">
<source>Reset</source>
<target>Reset</target>
<note>Section title for resetting the user</note>
</trans-unit>
<trans-unit id="River" xml:space="preserve">
<source>River</source>
<target>River</target>
<note>Dropdown option label for Lightning wallet, River</note>
</trans-unit>
<trans-unit id="Satoshi Nakamoto" xml:space="preserve">
<source>Satoshi Nakamoto</source>
<target>Satoshi Nakamoto</target>
<note>Name of Bitcoin creator(s).</note>
</trans-unit>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Save</target>
<note>Button for saving profile.</note>
</trans-unit>
<trans-unit id="Save Image" xml:space="preserve">
<source>Save Image</source>
<target>Save Image</target>
<note>Context menu option to save an image.</note>
</trans-unit>
<trans-unit id="Search hashtag: #%@" xml:space="preserve">
<source>Search hashtag: #%@</source>
<target>Search hashtag: #%@</target>
<note>Navigation link to search hashtag.</note>
</trans-unit>
<trans-unit id="Search..." xml:space="preserve">
<source>Search...</source>
<target>Search...</target>
<note>Placeholder text to prompt entry of search query.</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
<target>Secret Account Login Key</target>
<note>Section title for user's secret account login key.</note>
</trans-unit>
<trans-unit id="Select a Lightning wallet" xml:space="preserve">
<source>Select a Lightning wallet</source>
<target>Select a Lightning wallet</target>
<note>Title of section for selecting a Lightning wallet to pay a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Select default wallet" xml:space="preserve">
<source>Select default wallet</source>
<target>Select default wallet</target>
<note>Prompt selection of user's default wallet</note>
</trans-unit>
<trans-unit id="Send a message to start the conversation..." xml:space="preserve">
<source>Send a message to start the conversation...</source>
<target>Send a message to start the conversation...</target>
<note>Text prompt for user to send a message to the other user.</note>
</trans-unit>
<trans-unit id="Settings" xml:space="preserve">
<source>Settings</source>
<target>Settings</target>
<note>Navigation title for Settings view.</note>
</trans-unit>
<trans-unit id="Share" xml:space="preserve">
<source>Share</source>
<target>Share</target>
<note>Button to share an image.</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<target>Show</target>
<note>Toggle to show or hide user's secret account login key.</note>
</trans-unit>
<trans-unit id="Show wallet selector" xml:space="preserve">
<source>Show wallet selector</source>
<target>Show wallet selector</target>
<note>Toggle to show or hide selection of wallet.</note>
</trans-unit>
<trans-unit id="Strike" xml:space="preserve">
<source>Strike</source>
<target>Strike</target>
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
<source>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</source>
<target>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</target>
<note>Warning that the inputted account key is a public key and the result of what happens because of it.</note>
</trans-unit>
<trans-unit id="This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." xml:space="preserve">
<source>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</source>
<target>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</target>
<note>Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key.</note>
</trans-unit>
<trans-unit id="This is your account ID, you can give this to your friends so that they can follow you. Click to copy." xml:space="preserve">
<source>This is your account ID, you can give this to your friends so that they can follow you. Click to copy.</source>
<target>This is your account ID, you can give this to your friends so that they can follow you. Click to copy.</target>
<note>Label to describe that a public key is the user's account ID and what they can do with it.</note>
</trans-unit>
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
<target>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</target>
<note>Label to describe that a private key is the user's secret account key and what they should do with it.</note>
</trans-unit>
<trans-unit id="Thread" xml:space="preserve">
<source>Thread</source>
<target>Thread</target>
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</note>
</trans-unit>
<trans-unit id="Type your post here..." xml:space="preserve">
<source>Type your post here...</source>
<target>Type your post here...</target>
<note>Text box prompt to ask user to type their post.</note>
</trans-unit>
<trans-unit id="Unfollow" xml:space="preserve">
<source>Unfollow</source>
<target>Unfollow</target>
<note>Button to unfollow a user.</note>
</trans-unit>
<trans-unit id="Unfollowing" xml:space="preserve">
<source>Unfollowing</source>
<target>Unfollowing</target>
<note>Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.</note>
</trans-unit>
<trans-unit id="Unfollowing..." xml:space="preserve">
<source>Unfollowing...</source>
<target>Unfollowing...</target>
<note>Label to indicate that the user is in the process of unfollowing another user.</note>
</trans-unit>
<trans-unit id="Unfollows" xml:space="preserve">
<source>Unfollows</source>
<target>Unfollows</target>
<note>Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Username</target>
<note>Label for Username section of user profile form.
Label to prompt username entry.</note>
</trans-unit>
<trans-unit id="Wallet Of Satoshi" xml:space="preserve">
<source>Wallet Of Satoshi</source>
<target>Wallet Of Satoshi</target>
<note>Dropdown option label for Lightning wallet, Wallet Of Satoshi.</note>
</trans-unit>
<trans-unit id="Wallet Selector" xml:space="preserve">
<source>Wallet Selector</source>
<target>Wallet Selector</target>
<note>Section title for selection of wallet.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
<source>Website</source>
<target>Website</target>
<note>Label for Website section of user profile form.</note>
</trans-unit>
<trans-unit id="Welcome to the social network %@ control." xml:space="preserve">
<source>Welcome to the social network %@ control.</source>
<target>Welcome to the social network %@ control.</target>
<note>Welcoming message to the reader. The variable is 'you', the reader.</note>
</trans-unit>
<trans-unit id="Welcome, %@!" xml:space="preserve">
<source>Welcome, %@!</source>
<target>Welcome, %@!</target>
<note>Text to welcome user.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<target>Your Name</target>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<target>Zebedee</target>
<note>Dropdown option label for Lightning wallet, Zebedee.</note>
</trans-unit>
<trans-unit id="Zeus LN" xml:space="preserve">
<source>Zeus LN</source>
<target>Zeus LN</target>
<note>Dropdown option label for Lightning wallet, Zeus LN.</note>
</trans-unit>
<trans-unit id="collapsed_event_view_other_notes" translate="no" xml:space="preserve">
<source>collapsed_event_view_other_notes</source>
<target>collapsed_event_view_other_notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
<source>https://example.com/pic.jpg</source>
<target>https://example.com/pic.jpg</target>
<note>Placeholder example text for profile picture URL.</note>
</trans-unit>
<trans-unit id="https://jb55.com" xml:space="preserve">
<source>https://jb55.com</source>
<target>https://jb55.com</target>
<note>Placeholder example text for website URL for user profile.</note>
</trans-unit>
<trans-unit id="jb55@jb55.com" xml:space="preserve">
<source>jb55@jb55.com</source>
<target>jb55@jb55.com</target>
<note>Placeholder example text for identifier used for NIP-05 verification.</note>
</trans-unit>
<trans-unit id="none" xml:space="preserve">
<source>none</source>
<target>none</target>
<note>No search results.</note>
</trans-unit>
<trans-unit id="now" xml:space="preserve">
<source>now</source>
<target>now</target>
<note>String indicating that a given timestamp just occurred</note>
</trans-unit>
<trans-unit id="nsec1..." xml:space="preserve">
<source>nsec1...</source>
<target>nsec1...</target>
<note>Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key.</note>
</trans-unit>
<trans-unit id="optional" xml:space="preserve">
<source>optional</source>
<target>optional</target>
<note>Label indicating that a form input is optional.</note>
</trans-unit>
<trans-unit id="replying_to_one_and_others" translate="no" xml:space="preserve">
<source>replying_to_one_and_others</source>
<target>replying_to_one_and_others</target>
<note>Label to indicate that the user is replying to 1 user and others. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="replying_to_two_and_others" translate="no" xml:space="preserve">
<source>replying_to_two_and_others</source>
<target>replying_to_two_and_others</target>
<note>Label to indicate that the user is replying to 2 users and others. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="satoshi" xml:space="preserve">
<source>satoshi</source>
<target>satoshi</target>
<note>Example username of Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="wss://some.relay.com" xml:space="preserve">
<source>wss://some.relay.com</source>
<target>wss://some.relay.com</target>
<note>Placeholder example for relay server address.</note>
</trans-unit>
<trans-unit id="you" xml:space="preserve">
<source>you</source>
<target>you</target>
<note>You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself.</note>
</trans-unit>
<trans-unit id="🤙" xml:space="preserve">
<source>🤙</source>
<target>🤙</target>
<note>Button with emoji to like an event.</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/one:dict/:string" xml:space="preserve">
<source>1 other note</source>
<target>1 other note</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/other:dict/:string" xml:space="preserve">
<source>%d other notes</source>
<target>%d other notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/zero:dict/:string" xml:space="preserve">
<source>0 other notes</source>
<target>0 other notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>··· %#@NOTES@ ···</source>
<target>··· %#@NOTES@ ···</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@%#@OTHERS@</source>
<target>Replying to %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/one:dict/:string" xml:space="preserve">
<source> &amp; 1 other</source>
<target> &amp; 1 other</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/other:dict/:string" xml:space="preserve">
<source> &amp; %d others</source>
<target> &amp; %d others</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/zero:dict/:string" xml:space="preserve">
<source/>
<target/>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@, %@%#@OTHERS@</source>
<target>Replying to %@, %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/one:dict/:string" xml:space="preserve">
<source> &amp; 1 other</source>
<target> &amp; 1 other</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/other:dict/:string" xml:space="preserve">
<source> &amp; %d others</source>
<target> &amp; %d others</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/zero:dict/:string" xml:space="preserve">
<source/>
<target/>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,6 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "\"Granting Damus access to your photo library allows you to save photos.";

View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>0 other notes</string>
<key>one</key>
<string>1 other note</string>
<key>other</key>
<string>%d other notes</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,743 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="damus/en-US.lproj/InfoPlist.strings" datatype="plaintext" source-language="en-US" target-language="es-419">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
<source>Damus</source>
<note>Bundle display name</note>
</trans-unit>
<trans-unit id="CFBundleName" xml:space="preserve">
<source>damus</source>
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>"Granting Damus access to your photo library allows you to save photos.</source>
<note>Privacy - Photo Library Additions Usage Description</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="es-419" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<note>Blank space to separate profile picture from profile editor form.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<note>Number of people following a user.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<note>Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.</note>
</trans-unit>
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
<note>Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" xml:space="preserve">
<source>%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs</source>
<note>Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet." xml:space="preserve">
<source>%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.</source>
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%lld" xml:space="preserve">
<source>%lld</source>
<note>Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
<source>%lld/%lld</source>
<note>Fraction of how many of the user's relay servers that are operational.</note>
</trans-unit>
<trans-unit id="&amp;nbsp;" xml:space="preserve">
<source>&amp;nbsp;</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="'%@' at '%@' will be used for verification" xml:space="preserve">
<source>'%@' at '%@' will be used for verification</source>
<note>Description of how the nip05 identifier would be used for verification.</note>
</trans-unit>
<trans-unit id="'%@' is an invalid nip05 identifier. It should look like an email." xml:space="preserve">
<source>'%@' is an invalid nip05 identifier. It should look like an email.</source>
<note>Description of why the nip05 identifier is invalid.</note>
</trans-unit>
<trans-unit id="(Profile.displayName(profile: profile, pubkey: whos))'s Followers" xml:space="preserve">
<source>(Profile.displayName(profile: profile, pubkey: whos))'s Followers</source>
<note>Navigation bar title for view that shows who is following a user.</note>
</trans-unit>
<trans-unit id="(formattedSats) sat" xml:space="preserve">
<source>(formattedSats) sat</source>
<note>Amount of 1 sat.</note>
</trans-unit>
<trans-unit id="(formattedSats) sats" xml:space="preserve">
<source>(formattedSats) sats</source>
<note>Amount of sats.</note>
</trans-unit>
<trans-unit id="(who) following" xml:space="preserve">
<source>(who) following</source>
<note>Navigation bar title for view that shows who a user is following.</note>
</trans-unit>
<trans-unit id="&lt; e &gt;" xml:space="preserve">
<source>&lt; e &gt;</source>
<note>Placeholder for event mention.</note>
</trans-unit>
<trans-unit id="@" xml:space="preserve">
<source>@</source>
<note>Prefix character to username.</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
<note>Label to prompt for about text entry for user to describe about themself.</note>
</trans-unit>
<trans-unit id="About Me" xml:space="preserve">
<source>About Me</source>
<note>Label for About Me section of user profile form.</note>
</trans-unit>
<trans-unit id="Absolute Boss" xml:space="preserve">
<source>Absolute Boss</source>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Account ID" xml:space="preserve">
<source>Account ID</source>
<note>Label to indicate the public ID of the account.</note>
</trans-unit>
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<note>Button to add recommended relay server.
Button to confirm adding user inputted relay.</note>
</trans-unit>
<trans-unit id="Add Relay" xml:space="preserve">
<source>Add Relay</source>
<note>Label for section for adding a relay server.</note>
</trans-unit>
<trans-unit id="Any" xml:space="preserve">
<source>Any</source>
<note>Any amount of sats</note>
</trans-unit>
<trans-unit id="Are you sure you want to boost this post?" xml:space="preserve">
<source>Are you sure you want to boost this post?</source>
<note>Alert message to ask if user wants to boost a post.</note>
</trans-unit>
<trans-unit id="Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." xml:space="preserve">
<source>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</source>
<note>Reminder to user that they should save their account information.</note>
</trans-unit>
<trans-unit id="Bitcoin Beach" xml:space="preserve">
<source>Bitcoin Beach</source>
<note>Dropdown option label for Lightning wallet, Bitcoin Beach.</note>
</trans-unit>
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
<source>Bitcoin Lightning Tips</source>
<note>Label for Bitcoin Lightning Tips section of user profile form.</note>
</trans-unit>
<trans-unit id="Blixt Wallet" xml:space="preserve">
<source>Blixt Wallet</source>
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
</trans-unit>
<trans-unit id="Blue Wallet" xml:space="preserve">
<source>Blue Wallet</source>
<note>Dropdown option label for Lightning wallet, Blue Wallet.</note>
</trans-unit>
<trans-unit id="Boost" xml:space="preserve">
<source>Boost</source>
<note>Button to confirm boosting a post.
Title of alert for confirming to boost a post.</note>
</trans-unit>
<trans-unit id="Boosted" xml:space="preserve">
<source>Boosted</source>
<note>Text indicating that the post was boosted (i.e. re-shared).</note>
</trans-unit>
<trans-unit id="Breez" xml:space="preserve">
<source>Breez</source>
<note>Dropdown option label for Lightning wallet, Breez.</note>
</trans-unit>
<trans-unit id="Broadcast" xml:space="preserve">
<source>Broadcast</source>
<note>Context menu option for broadcasting the user's note to all of the user's connected relay servers.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<note>Button to cancel out of posting a note.
Button to cancel out of view adding user inputted relay.
Cancel out of logging out the user.</note>
</trans-unit>
<trans-unit id="Cash App" xml:space="preserve">
<source>Cash App</source>
<note>Dropdown option label for Lightning wallet, Cash App.</note>
</trans-unit>
<trans-unit id="Chat" xml:space="preserve">
<source>Chat</source>
<note>Navigation bar title for Chatroom view.</note>
</trans-unit>
<trans-unit id="Clear" xml:space="preserve">
<source>Clear</source>
<note>Button for clearing cached data.</note>
</trans-unit>
<trans-unit id="Clear Cache" xml:space="preserve">
<source>Clear Cache</source>
<note>Section title for clearing cached data.</note>
</trans-unit>
<trans-unit id="Copied" xml:space="preserve">
<source>Copied</source>
<note>Label indicating that a user's key was copied.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
<source>Copy</source>
<note>Button to copy a relay server address.</note>
</trans-unit>
<trans-unit id="Copy Account ID" xml:space="preserve">
<source>Copy Account ID</source>
<note>Context menu option for copying the ID of the account that created the note.</note>
</trans-unit>
<trans-unit id="Copy Image" xml:space="preserve">
<source>Copy Image</source>
<note>Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard.</note>
</trans-unit>
<trans-unit id="Copy Image URL" xml:space="preserve">
<source>Copy Image URL</source>
<note>Context menu option to copy the URL of an image into clipboard.</note>
</trans-unit>
<trans-unit id="Copy LNURL" xml:space="preserve">
<source>Copy LNURL</source>
<note>Context menu option for copying a user's Lightning URL.</note>
</trans-unit>
<trans-unit id="Copy Note ID" xml:space="preserve">
<source>Copy Note ID</source>
<note>Context menu option for copying the ID of the note.</note>
</trans-unit>
<trans-unit id="Copy Note JSON" xml:space="preserve">
<source>Copy Note JSON</source>
<note>Context menu option for copying the JSON text from the note.</note>
</trans-unit>
<trans-unit id="Copy Text" xml:space="preserve">
<source>Copy Text</source>
<note>Context menu option for copying the text from an note.</note>
</trans-unit>
<trans-unit id="Copy User ID" xml:space="preserve">
<source>Copy User ID</source>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
<trans-unit id="Copy invoice" xml:space="preserve">
<source>Copy invoice</source>
<note>Title of section for copying a Lightning invoice identifier.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve">
<source>Create</source>
<note>Button to create account.</note>
</trans-unit>
<trans-unit id="Create Account" xml:space="preserve">
<source>Create Account</source>
<note>Button to create an account.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="DM" xml:space="preserve">
<source>DM</source>
<note>Navigation title for DM view, which is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Damus" xml:space="preserve">
<source>Damus</source>
<note>Name of the app, shown on the first screen when user is not logged in.</note>
</trans-unit>
<trans-unit id="Default Wallet" xml:space="preserve">
<source>Default Wallet</source>
<note>Button to pay a Lightning invoice with the user's default Lightning wallet.</note>
</trans-unit>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
<note>Button to delete a relay server that the user connects to.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
<note>Button to dismiss a text field alert.</note>
</trans-unit>
<trans-unit id="Display Name" xml:space="preserve">
<source>Display Name</source>
<note>Label to prompt display name entry.</note>
</trans-unit>
<trans-unit id="Done" xml:space="preserve">
<source>Done</source>
<note>Button to dismiss wallet selection view for paying Lightning invoice.</note>
</trans-unit>
<trans-unit id="Earn Money" xml:space="preserve">
<source>Earn Money</source>
<note>Heading indicating that this application allows users to earn money.</note>
</trans-unit>
<trans-unit id="Edit" xml:space="preserve">
<source>Edit</source>
<note>Button to edit user's profile.</note>
</trans-unit>
<trans-unit id="Edit Profile" xml:space="preserve">
<source>Edit Profile</source>
<note>Title of navigation view for Edit Profile.</note>
</trans-unit>
<trans-unit id="Encrypted" xml:space="preserve">
<source>Encrypted</source>
<note>Heading indicating that this application keeps private messaging end-to-end encrypted.</note>
</trans-unit>
<trans-unit id="Encrypted DMs" xml:space="preserve">
<source>Encrypted DMs</source>
<note>Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Enter your account key to login:" xml:space="preserve">
<source>Enter your account key to login:</source>
<note>Prompt for user to enter an account key to login.</note>
</trans-unit>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<note>Error message indicating why saving keys failed.</note>
</trans-unit>
<trans-unit id="Filter State" xml:space="preserve">
<source>Filter State</source>
<note>Filter state for seeing either only posts, or posts &amp; replies.</note>
</trans-unit>
<trans-unit id="Follow" xml:space="preserve">
<source>Follow</source>
<note>Button to follow a user.</note>
</trans-unit>
<trans-unit id="Followers" xml:space="preserve">
<source>Followers</source>
<note>Label describing followers of a user.</note>
</trans-unit>
<trans-unit id="Following" xml:space="preserve">
<source>Following</source>
<note>Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following.</note>
</trans-unit>
<trans-unit id="Following..." xml:space="preserve">
<source>Following...</source>
<note>Label to indicate that the user is in the process of following another user.</note>
</trans-unit>
<trans-unit id="Follows" xml:space="preserve">
<source>Follows</source>
<note>Text to indicate that button next to it is in a state that will follow a profile when tapped.</note>
</trans-unit>
<trans-unit id="Global" xml:space="preserve">
<source>Global</source>
<note>Navigation bar title for Global view where posts from all connected relay servers appear.</note>
</trans-unit>
<trans-unit id="Goto post %@" xml:space="preserve">
<source>Goto post %@</source>
<note>Navigation link to go to post referenced by hex code.</note>
</trans-unit>
<trans-unit id="Goto profile %@" xml:space="preserve">
<source>Goto profile %@</source>
<note>Navigation link to go to profile.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<note>Navigation bar title for Home view where posts and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Invalid key" xml:space="preserve">
<source>Invalid key</source>
<note>Error message indicating that an invalid account key was entered for login.</note>
</trans-unit>
<trans-unit id="LNLink" xml:space="preserve">
<source>LNLink</source>
<note>Dropdown option label for Lightning wallet, LNLink.</note>
</trans-unit>
<trans-unit id="Let's go!" xml:space="preserve">
<source>Let's go!</source>
<note>Button to complete account creation and start using the app.</note>
</trans-unit>
<trans-unit id="Lightning Address or LNURL" xml:space="preserve">
<source>Lightning Address or LNURL</source>
<note>Placeholder text for entry of Lightning Address or LNURL.</note>
</trans-unit>
<trans-unit id="Lightning Invoice" xml:space="preserve">
<source>Lightning Invoice</source>
<note>Indicates that the view is for paying a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Local default" xml:space="preserve">
<source>Local default</source>
<note>Dropdown option label for system default for Lightning wallet.</note>
</trans-unit>
<trans-unit id="Login" xml:space="preserve">
<source>Login</source>
<note>Button to log into account.</note>
</trans-unit>
<trans-unit id="Logout" xml:space="preserve">
<source>Logout</source>
<note>Alert for logging out the user.
Button for logging out the user.
Button to logout the user.</note>
</trans-unit>
<trans-unit id="Make sure your nsec account key is saved before you logout or you will lose access to this account" xml:space="preserve">
<source>Make sure your nsec account key is saved before you logout or you will lose access to this account</source>
<note>Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.</note>
</trans-unit>
<trans-unit id="Muun" xml:space="preserve">
<source>Muun</source>
<note>Dropdown option label for Lightning wallet, Muun.</note>
</trans-unit>
<trans-unit id="NIP-05 Verification" xml:space="preserve">
<source>NIP-05 Verification</source>
<note>Label for NIP-05 Verification section of user profile form.</note>
</trans-unit>
<trans-unit id="Nothing to see here. Check back later!" xml:space="preserve">
<source>Nothing to see here. Check back later!</source>
<note>Indicates that there are no notes in the timeline to view.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<note>Navigation title for notifications.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
<note>Button to pay a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Pay the Lightning invoice" xml:space="preserve">
<source>Pay the Lightning invoice</source>
<note>Navigation bar title for view to pay Lightning invoice.</note>
</trans-unit>
<trans-unit id="Phoenix" xml:space="preserve">
<source>Phoenix</source>
<note>Dropdown option label for Lightning wallet, Phoenix.</note>
</trans-unit>
<trans-unit id="Post" xml:space="preserve">
<source>Post</source>
<note>Button to post a note.</note>
</trans-unit>
<trans-unit id="Posts" xml:space="preserve">
<source>Posts</source>
<note>Label for filter for seeing only posts (instead of posts and replies).</note>
</trans-unit>
<trans-unit id="Posts &amp; Replies" xml:space="preserve">
<source>Posts &amp; Replies</source>
<note>Label for filter for seeing posts and replies (instead of only posts).</note>
</trans-unit>
<trans-unit id="Private" xml:space="preserve">
<source>Private</source>
<note>Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.</note>
</trans-unit>
<trans-unit id="Private Key" xml:space="preserve">
<source>Private Key</source>
<note>Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.</note>
</trans-unit>
<trans-unit id="PrivateKey" xml:space="preserve">
<source>PrivateKey</source>
<note>Title of the secure field that holds the user's private key.</note>
</trans-unit>
<trans-unit id="Profile Picture" xml:space="preserve">
<source>Profile Picture</source>
<note>Label for Profile Picture section of user profile form.</note>
</trans-unit>
<trans-unit id="Public Account ID" xml:space="preserve">
<source>Public Account ID</source>
<note>Section title for the user's public account ID.</note>
</trans-unit>
<trans-unit id="Public Key" xml:space="preserve">
<source>Public Key</source>
<note>Label indicating that the text is a user's public account key.</note>
</trans-unit>
<trans-unit id="Public Key?" xml:space="preserve">
<source>Public Key?</source>
<note>Prompt to ask user if the key they entered is a public key.</note>
</trans-unit>
<trans-unit id="Public key" xml:space="preserve">
<source>Public key</source>
<note>Label indicating that the text is a user's public account key.</note>
</trans-unit>
<trans-unit id="Recommended Relays" xml:space="preserve">
<source>Recommended Relays</source>
<note>Section title for recommend relay servers that could be added as part of configuration</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<note>Text field for relay server. Used for testing purposes.</note>
</trans-unit>
<trans-unit id="Relays" xml:space="preserve">
<source>Relays</source>
<note>Header text for relay server list for configuration.
Part of a larger sentence to describe how many relay servers a user is connected.</note>
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<note>Label to indicate that the user is replying to themself.</note>
</trans-unit>
<trans-unit id="Replying to %@ &amp; %@" xml:space="preserve">
<source>Replying to %1$@ &amp; %2$@</source>
<note>Label to indicate that the user is replying to 2 users.</note>
</trans-unit>
<trans-unit id="Replying to:" xml:space="preserve">
<source>Replying to:</source>
<note>Indicating that the user is replying to the following listed people.</note>
</trans-unit>
<trans-unit id="Reset" xml:space="preserve">
<source>Reset</source>
<note>Section title for resetting the user</note>
</trans-unit>
<trans-unit id="River" xml:space="preserve">
<source>River</source>
<note>Dropdown option label for Lightning wallet, River</note>
</trans-unit>
<trans-unit id="Satoshi Nakamoto" xml:space="preserve">
<source>Satoshi Nakamoto</source>
<note>Name of Bitcoin creator(s).</note>
</trans-unit>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<note>Button for saving profile.</note>
</trans-unit>
<trans-unit id="Save Image" xml:space="preserve">
<source>Save Image</source>
<note>Context menu option to save an image.</note>
</trans-unit>
<trans-unit id="Search hashtag: #%@" xml:space="preserve">
<source>Search hashtag: #%@</source>
<note>Navigation link to search hashtag.</note>
</trans-unit>
<trans-unit id="Search..." xml:space="preserve">
<source>Search...</source>
<note>Placeholder text to prompt entry of search query.</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
<note>Section title for user's secret account login key.</note>
</trans-unit>
<trans-unit id="Select a Lightning wallet" xml:space="preserve">
<source>Select a Lightning wallet</source>
<note>Title of section for selecting a Lightning wallet to pay a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Select default wallet" xml:space="preserve">
<source>Select default wallet</source>
<note>Prompt selection of user's default wallet</note>
</trans-unit>
<trans-unit id="Send a message to start the conversation..." xml:space="preserve">
<source>Send a message to start the conversation...</source>
<note>Text prompt for user to send a message to the other user.</note>
</trans-unit>
<trans-unit id="Settings" xml:space="preserve">
<source>Settings</source>
<note>Navigation title for Settings view.</note>
</trans-unit>
<trans-unit id="Share" xml:space="preserve">
<source>Share</source>
<note>Button to share an image.</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<note>Toggle to show or hide user's secret account login key.</note>
</trans-unit>
<trans-unit id="Show wallet selector" xml:space="preserve">
<source>Show wallet selector</source>
<note>Toggle to show or hide selection of wallet.</note>
</trans-unit>
<trans-unit id="Strike" xml:space="preserve">
<source>Strike</source>
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
<source>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</source>
<note>Warning that the inputted account key is a public key and the result of what happens because of it.</note>
</trans-unit>
<trans-unit id="This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." xml:space="preserve">
<source>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</source>
<note>Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key.</note>
</trans-unit>
<trans-unit id="This is your account ID, you can give this to your friends so that they can follow you. Click to copy." xml:space="preserve">
<source>This is your account ID, you can give this to your friends so that they can follow you. Click to copy.</source>
<note>Label to describe that a public key is the user's account ID and what they can do with it.</note>
</trans-unit>
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
<note>Label to describe that a private key is the user's secret account key and what they should do with it.</note>
</trans-unit>
<trans-unit id="Thread" xml:space="preserve">
<source>Thread</source>
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</note>
</trans-unit>
<trans-unit id="Type your post here..." xml:space="preserve">
<source>Type your post here...</source>
<note>Text box prompt to ask user to type their post.</note>
</trans-unit>
<trans-unit id="Unfollow" xml:space="preserve">
<source>Unfollow</source>
<note>Button to unfollow a user.</note>
</trans-unit>
<trans-unit id="Unfollowing" xml:space="preserve">
<source>Unfollowing</source>
<note>Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.</note>
</trans-unit>
<trans-unit id="Unfollowing..." xml:space="preserve">
<source>Unfollowing...</source>
<note>Label to indicate that the user is in the process of unfollowing another user.</note>
</trans-unit>
<trans-unit id="Unfollows" xml:space="preserve">
<source>Unfollows</source>
<note>Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>Label for Username section of user profile form.
Label to prompt username entry.</note>
</trans-unit>
<trans-unit id="Wallet Of Satoshi" xml:space="preserve">
<source>Wallet Of Satoshi</source>
<note>Dropdown option label for Lightning wallet, Wallet Of Satoshi.</note>
</trans-unit>
<trans-unit id="Wallet Selector" xml:space="preserve">
<source>Wallet Selector</source>
<note>Section title for selection of wallet.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
<source>Website</source>
<note>Label for Website section of user profile form.</note>
</trans-unit>
<trans-unit id="Welcome to the social network %@ control." xml:space="preserve">
<source>Welcome to the social network %@ control.</source>
<note>Welcoming message to the reader. The variable is 'you', the reader.</note>
</trans-unit>
<trans-unit id="Welcome, %@!" xml:space="preserve">
<source>Welcome, %@!</source>
<note>Text to welcome user.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<note>Dropdown option label for Lightning wallet, Zebedee.</note>
</trans-unit>
<trans-unit id="Zeus LN" xml:space="preserve">
<source>Zeus LN</source>
<note>Dropdown option label for Lightning wallet, Zeus LN.</note>
</trans-unit>
<trans-unit id="collapsed_event_view_other_notes" translate="no" xml:space="preserve">
<source>collapsed_event_view_other_notes</source>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
<source>https://example.com/pic.jpg</source>
<note>Placeholder example text for profile picture URL.</note>
</trans-unit>
<trans-unit id="https://jb55.com" xml:space="preserve">
<source>https://jb55.com</source>
<note>Placeholder example text for website URL for user profile.</note>
</trans-unit>
<trans-unit id="jb55@jb55.com" xml:space="preserve">
<source>jb55@jb55.com</source>
<note>Placeholder example text for identifier used for NIP-05 verification.</note>
</trans-unit>
<trans-unit id="none" xml:space="preserve">
<source>none</source>
<note>No search results.</note>
</trans-unit>
<trans-unit id="now" xml:space="preserve">
<source>now</source>
<note>String indicating that a given timestamp just occurred</note>
</trans-unit>
<trans-unit id="nsec1..." xml:space="preserve">
<source>nsec1...</source>
<note>Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key.</note>
</trans-unit>
<trans-unit id="optional" xml:space="preserve">
<source>optional</source>
<note>Label indicating that a form input is optional.</note>
</trans-unit>
<trans-unit id="replying_to_one_and_others" translate="no" xml:space="preserve">
<source>replying_to_one_and_others</source>
<note>Label to indicate that the user is replying to 1 user and others. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="replying_to_two_and_others" translate="no" xml:space="preserve">
<source>replying_to_two_and_others</source>
<note>Label to indicate that the user is replying to 2 users and others. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="satoshi" xml:space="preserve">
<source>satoshi</source>
<note>Example username of Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="wss://some.relay.com" xml:space="preserve">
<source>wss://some.relay.com</source>
<note>Placeholder example for relay server address.</note>
</trans-unit>
<trans-unit id="you" xml:space="preserve">
<source>you</source>
<note>You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself.</note>
</trans-unit>
<trans-unit id="🤙" xml:space="preserve">
<source>🤙</source>
<note>Button with emoji to like an event.</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="es-419" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/one:dict/:string" xml:space="preserve">
<source>1 other note</source>
<target>1 other note</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/other:dict/:string" xml:space="preserve">
<source>%d other notes</source>
<target>%d other notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/zero:dict/:string" xml:space="preserve">
<source>0 other notes</source>
<target>0 other notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>··· %#@NOTES@ ···</source>
<target>··· %#@NOTES@ ···</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@%#@OTHERS@</source>
<target>Replying to %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/one:dict/:string" xml:space="preserve">
<source> &amp; 1 other</source>
<target> &amp; 1 other</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/other:dict/:string" xml:space="preserve">
<source> &amp; %d others</source>
<target> &amp; %d others</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/OTHERS:dict/zero:dict/:string" xml:space="preserve">
<source/>
<target/>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@, %@%#@OTHERS@</source>
<target>Replying to %@, %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/one:dict/:string" xml:space="preserve">
<source> &amp; 1 other</source>
<target> &amp; 1 other</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/other:dict/:string" xml:space="preserve">
<source> &amp; %d others</source>
<target> &amp; %d others</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/OTHERS:dict/zero:dict/:string" xml:space="preserve">
<source/>
<target/>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>0 other notes</string>
<key>one</key>
<string>1 other note</string>
<key>other</key>
<string>%d other notes</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,12 +0,0 @@
{
"developmentRegion" : "en-US",
"project" : "damus.xcodeproj",
"targetLocale" : "es-419",
"toolInfo" : {
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "14.2"
},
"version" : "1.0"
}

View File

@@ -22,6 +22,14 @@ static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
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;
@@ -29,18 +37,35 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
c->p = content;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
bool consumedAtLeastOne = false;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
}
@@ -145,7 +170,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
return 0;
}
consume_until_whitespace(cur, 1);
consume_until_boundary(cur);
block->type = BLOCK_HASHTAG;
block->block.str.start = (const char*)(start + 1);
@@ -200,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;

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEF227F7A08200C66700"
BuildableName = "damusTests.xctest"
BlueprintName = "damusTests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
BuildableName = "damusUITests.xctest"
BlueprintName = "damusUITests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

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

@@ -0,0 +1,60 @@
//
// CustomPicker.swift
// damus
//
// Created by Eric Holguin on 1/22/23.
//
import SwiftUI
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
Color("DamusPurple"),
Color("DamusBlue")
]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
@Environment(\.colorScheme) var colorScheme
@Namespace var picker
@Binding var selection: SelectionValue
@ViewBuilder let content: Content
public var body: some View {
let contentMirror = Mirror(reflecting: content)
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
HStack {
ForEach(0..<blocksCount, id: \.self) { index in
let tupleBlock = contentMirror.descendant("value", ".\(index)")
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
Button {
withAnimation(.spring()) {
selection = tag
}
} label: {
text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy))
}
.background(
Group {
if tag == selection {
Rectangle().fill(RECTANGLE_GRADIENT).frame(height: 2.5)
.matchedGeometryEffect(id: "selector", in: picker)
.cornerRadius(2.5)
}
},
alignment: .bottom
)
.frame(maxWidth: .infinity)
.accentColor(tag == selection ? textColor() : .gray)
}
}
}
func textColor() -> Color {
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
}

View File

@@ -0,0 +1,39 @@
//
// Highlight.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import Foundation
import SwiftUI
enum Highlight {
case none
case main
case reply
case custom(Color, Float)
var is_main: Bool {
if case .main = self {
return true
}
return false
}
var is_none: Bool {
if case .none = self {
return true
}
return false
}
var is_replied_to: Bool {
switch self {
case .reply: return true
default: return false
}
}
}

View File

@@ -12,14 +12,14 @@ import Kingfisher
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [URL]
let activityItems: [URL?]
let callback: Callback? = nil
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
activityItems: activityItems as [Any],
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
@@ -32,7 +32,7 @@ struct ShareSheet: UIViewControllerRepresentable {
}
struct ImageContextMenuModifier: ViewModifier {
let url: URL
let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
@@ -64,8 +64,12 @@ struct ImageContextMenuModifier: ViewModifier {
}
}
struct ImageViewer: View {
let urls: [URL]
private struct ImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -75,45 +79,110 @@ struct ImageViewer: View {
return image
}
}
@State private var image: UIImage?
@State private var showShareSheet = false
func onShared(completed: Bool) -> Void {
if (completed) {
showShareSheet = false
var body: some View {
KFAnimatedImage(url)
.imageContext(.note)
.configure { view in
view.framePreloadCount = 3
}
.imageModifier(ImageHandler(handler: $image))
.clipped()
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
}
}
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
Divider()
.ignoresSafeArea()
}
.background(.regularMaterial)
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
TabView {
ForEach(urls, id: \.absoluteString) { url in
VStack{
Text(url.lastPathComponent)
KFAnimatedImage(url)
.configure { view in
view.framePreloadCount = 3
}
.cacheOriginalImage()
.imageModifier(ImageHandler(handler: $image))
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
VStack {
if showMenu {
navBarView
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
)
}
.tabViewStyle(PageTabViewStyle())
}
}
@@ -130,14 +199,13 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.tabItem {
Text(url.absoluteString)
}
@@ -150,11 +218,11 @@ struct ImageCarousel: View {
}
}
}
.cornerRadius(10)
.sheet(isPresented: $open_sheet) {
ImageViewer(urls: urls)
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: 200)
.clipped()
.onTapGesture {
open_sheet = true
}
@@ -164,6 +232,6 @@ struct ImageCarousel: View {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}

View File

@@ -7,6 +7,84 @@
import SwiftUI
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let our_pubkey: String
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@State var copied = false
var CopyButton: some View {
Button {
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
copied = false
}
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
UIPasteboard.general.string = invoice.string
} label: {
if !copied {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
} else {
Image(systemName: "checkmark.circle")
.foregroundColor(Color("DamusGreen"))
}
}
}
var PayButton: some View {
Button {
if should_show_wallet_selector(our_pubkey) {
showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20, style: .circular)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
print("pay button tap")
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
Spacer()
CopyButton
}
Divider()
Text(invoice.description_string)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
@@ -28,68 +106,12 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
}
}
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@ObservedObject var user_settings = UserSettingsStore()
var PayButton: some View {
Button {
if user_settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
//.buttonStyle(.bordered)
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
}
Divider()
Text(invoice.description)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
}
}
}
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(invoice: test_invoice)
.frame(width: 200, height: 200)
InvoiceView(our_pubkey: "", invoice: test_invoice)
.frame(width: 300, height: 200)
}
}

View File

@@ -8,6 +8,7 @@
import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
var invoices: [Invoice]
@State var open_sheet: Bool = false
@@ -16,7 +17,7 @@ struct InvoicesView: View {
var body: some View {
TabView {
ForEach(invoices, id: \.string) { invoice in
InvoiceView(invoice: invoice)
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
.tabItem {
Text(invoice.string)
}
@@ -30,7 +31,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
.frame(width: 300)
}
}

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

@@ -0,0 +1,145 @@
//
// TranslateButton.swift
// damus
//
// Created by William Casarin on 2023-02-02.
//
import SwiftUI
import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
@State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
@State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
}
.translate_button_style()
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content)
}
}
func CheckingStatus(lang: String) -> some View {
return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
}
func MainContent(note_lang: String) -> some View {
return Group {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let lang = languageName, show_translated_note {
if checkingTranslationStatus {
CheckingStatus(lang: lang)
} else if let artifacts = translated_artifacts {
Translated(lang: lang, artifacts: artifacts)
}
} else {
TranslateButton
}
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang)
} else {
Text("")
}
}
.task {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
checkingTranslationStatus = true
if #available(iOS 16, *) {
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
currentLanguage = Locale.current.languageCode ?? "en"
}
// 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 translation services typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
} else {
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
}
}
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
translated_note = nil
checkingTranslationStatus = false
return
}
if note_lang != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
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
translated_note = nil
}
}
if let translated = translated_note {
// Render translated note.
let translatedBlocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
}
}
}
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event)
}
}

View File

@@ -0,0 +1,38 @@
//
// UserView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct UserView: View {
let damus_state: DamusState
let pubkey: String
var body: some View {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
.buttonStyle(PlainButtonStyle())
}
}
struct UserView_Previews: PreviewProvider {
static var previews: some View {
UserView(damus_state: test_damus_state(), pubkey: "pk")
}
}

View File

@@ -0,0 +1,38 @@
//
// WebsiteLink.swift
// damus
//
// Created by William Casarin on 2023-01-22.
//
import SwiftUI
struct WebsiteLink: View {
let url: URL
@Environment(\.openURL) var openURL
var body: some View {
HStack {
Image(systemName: "link")
.foregroundColor(.gray)
.font(.footnote)
Button(action: {
openURL(url)
}, label: {
Text(link_text)
.font(.footnote)
})
}
}
var link_text: String {
url.host ?? url.absoluteString
}
}
struct WebsiteLink_Previews: PreviewProvider {
static var previews: some View {
WebsiteLink(url: URL(string: "https://jb55.com")!)
}
}

View File

@@ -0,0 +1,132 @@
//
// ZapButton.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import SwiftUI
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
let lnurl: String
@ObservedObject var bar: ActionBarModel
@State var zapping: Bool = false
@State var invoice: String = ""
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
func send_zap() {
guard let privkey = damus_state.keypair.privkey else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
// TODO: gather comment?
let content = ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
zapping = true
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
zapping = false
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
}
//damus_state.pool.send(.event(zapreq))
}
var zap_img: String {
if bar.zapped {
return "bolt.fill"
}
if !zapping {
return "bolt"
}
return "bolt.horizontal.fill"
}
var zap_color: Color? {
if bar.zapped {
return Color.orange
}
if !zapping {
return nil
}
return Color.yellow
}
var body: some View {
HStack(spacing: 4) {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
send_zap()
}
}
.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)
}
}
}
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
}
}

View File

@@ -0,0 +1,152 @@
//
// ZoomableScrollView.swift
// damus
//
// Created by Oleg Abalonski on 1/25/23.
//
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = GesturedScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let viewSize = hostingController.view.frame.size
guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return }
if scrollView.zoomScale > 1 {
let ratioW = viewSize.width / imageSize.width
let ratioH = viewSize.height / imageSize.height
let ratio = ratioW < ratioH ? ratioW:ratioH
let newWidth = imageSize.width * ratio
let newHeight = imageSize.height * ratio
let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width))
let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height))
scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
} else {
scrollView.contentInset = .zero
}
}
}
}
fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate {
let doubleTapGesture: UITapGestureRecognizer
override init(frame: CGRect) {
doubleTapGesture = UITapGestureRecognizer()
super.init(frame: frame)
doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap))
doubleTapGesture.numberOfTapsRequired = 2
addGestureRecognizer(doubleTapGesture)
doubleTapGesture.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
if self.zoomScale == 1 {
let pointInView = gesture.location(in: self.subviews.first)
let newZoomScale = self.maximumZoomScale / 4.0
let scrollViewSize = self.bounds.size
let width = scrollViewSize.width / newZoomScale
let height = scrollViewSize.height / newZoomScale
let originX = pointInView.x - (width / 2.0)
let originY = pointInView.y - (height / 2.0)
let zoomRect = CGRect(x: originX, y: originY, width: width, height: height)
self.zoom(to: zoomRect, animated: true)
} else {
self.setZoomScale(self.minimumZoomScale, animated: true)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == doubleTapGesture
}
}
fileprivate extension UIHostingController {
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}

View File

@@ -7,15 +7,14 @@
import SwiftUI
import Starscream
import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://nostr.v0l.io",
"wss://eden.nostr.land",
"wss://relay.snort.social",
"wss://offchain.pub",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
]
@@ -26,12 +25,18 @@ struct TimestampedProfile {
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"
}
}
}
@@ -71,6 +76,7 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@State var active_profile: String? = nil
@@ -79,11 +85,14 @@ struct ContentView: View {
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var blocking: String? = nil
@State var confirm_block: Bool = false
@State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@StateObject var user_settings = UserSettingsStore()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -93,49 +102,72 @@ struct ContentView: View {
var PostingTimelineView: some View {
VStack {
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
ZStack {
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.safeAreaInset(edge: .top) {
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
FiltersView
//.frame(maxWidth: 275)
.padding()
CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
}
.ignoresSafeArea(.keyboard)
}
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)
}
if privkey != nil {
PostButtonContainer(userSettings: user_settings) {
self.active_sheet = .post
}
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
}
}
var FiltersView: some View {
VStack{
Picker(NSLocalizedString("Filter State", comment: "Filter state for seeing either only posts, or posts & replies."), selection: $filter_state) {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
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: "")
}
.pickerStyle(.segmented)
}
}
@@ -158,9 +190,10 @@ struct ContentView: View {
PostingTimelineView
case .notifications:
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
VStack(spacing: 0) {
Divider()
NotificationsView(state: damus, notifications: home.notifications)
}
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
@@ -169,26 +202,21 @@ 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) {
if selected_timeline == .home {
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
} else {
Text("Global")
}
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
}
.ignoresSafeArea(.keyboard)
}
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()
}
@@ -217,46 +245,70 @@ struct ContentView: View {
}
}
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let ds = damus_state {
if let sec = ds.keypair.privkey {
ReportView(pool: ds.pool, target: target, privkey: sec)
} else {
EmptyView()
}
} else {
EmptyView()
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
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: {
if let picture = damus_state?.profiles.lookup(id: pubkey)?.picture {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, picture: picture)
} else {
Image(systemName: "person.fill")
}
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) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.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)
@@ -266,15 +318,27 @@ struct ContentView: View {
}
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
setup_notifications()
}
.sheet(item: $active_sheet) { item in
switch item {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, references: [])
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
@@ -318,6 +382,18 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
}
.onReceive(handle_notify(.report)) { notif in
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
}
.onReceive(handle_notify(.block)) { notif in
let pubkey = notif.object as! String
self.blocking = pubkey
self.confirm_block = true
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
self.damus_state?.pool.send(.event(ev))
@@ -381,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))
@@ -392,9 +470,100 @@ struct ContentView: View {
.onReceive(timer) { n in
self.damus_state?.pool.connect_to_disconnected()
}
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
notify(.logout, ())
}
}
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
}, message: {
if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
} else {
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false
confirm_block = false
}
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
guard let ds = damus_state else {
return
}
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = blocking else {
return
}
guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(mutelist)
ds.pool.send(.event(mutelist))
confirm_overwrite_mutelist = false
confirm_block = false
user_blocked_confirm = true
}
}, message: {
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
})
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
confirm_block = false
}
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
guard let ds = damus_state else {
return
}
if ds.contacts.mutelist == nil {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = blocking else {
return
}
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.pool.send(.event(ev))
}
}
}, message: {
if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
} else {
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
}
})
}
func switch_timeline(_ timeline: Timeline) {
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
if timeline == self.selected_timeline {
@@ -420,21 +589,34 @@ struct ContentView: View {
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in BOOTSTRAP_RELAYS {
add_relay(pool, relay)
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache()
)
home.damus_state = self.damus_state!

View File

@@ -15,8 +15,6 @@
</array>
</dict>
</array>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>&quot;Granting Damus access to your photo library allows you to save photos.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>river</string>
@@ -26,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

@@ -11,30 +11,43 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_tip: NostrEvent?
@Published var our_zap: Zap?
@Published var likes: Int
@Published var boosts: Int
@Published var tips: Int64
@Published var zaps: Int
@Published var zap_total: Int64
static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
}
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
self.likes = likes
self.boosts = boosts
self.tips = tips
self.zaps = zaps
self.zap_total = zap_total
self.our_like = our_like
self.our_boost = our_boost
self.our_tip = our_tip
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 && tips == 0
return likes == 0 && boosts == 0 && zaps == 0
}
var tipped: Bool {
return our_tip != nil
var zapped: Bool {
return our_zap != nil
}
var liked: Bool {

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

@@ -11,13 +11,51 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
private var muted: Set<String> = Set()
let our_pubkey: String
var event: NostrEvent?
var mutelist: NostrEvent?
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
func is_muted(_ pk: String) -> Bool {
return muted.contains(pk)
}
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
let diff = old.symmetricDifference(new)
var new_mutes = Array<String>()
var new_unmutes = Array<String>()
for d in diff {
if new.contains(d) {
new_mutes.append(d)
} else {
new_unmutes.append(d)
}
}
// TODO: set local mutelist here
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
if new_mutes.count > 0 {
notify(.new_mutes, new_mutes)
}
if new_unmutes.count > 0 {
notify(.new_unmutes, new_unmutes)
}
}
func get_friendosphere() -> [String] {
var fs = get_friend_list()
fs.append(contentsOf: get_friend_of_friend_list())

View File

@@ -18,12 +18,23 @@ struct DamusState {
let profiles: Profiles
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
let events: EventCache
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(), previews: PreviewCache())
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache())
}
}

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

@@ -8,13 +8,38 @@
import Foundation
class DirectMessageModel: ObservableObject {
@Published var events: [NostrEvent]
@Published var events: [NostrEvent] {
didSet {
is_request = determine_is_request()
}
}
@Published var draft: String
init(events: [NostrEvent]) {
self.events = events
var is_request: Bool
var our_pubkey: String
func determine_is_request() -> Bool {
for event in events {
if event.pubkey == our_pubkey {
return false
}
}
return true
}
init() {
init(events: [NostrEvent], our_pubkey: String) {
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

@@ -10,13 +10,26 @@ import Foundation
class DirectMessagesModel: ObservableObject {
@Published var dms: [(String, DirectMessageModel)] = []
@Published var loading: Bool = false
let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
var message_requests: [(String, DirectMessageModel)] {
return dms.filter { dm in dm.1.is_request }
}
var friend_dms: [(String, DirectMessageModel)] {
return dms.filter { dm in !dm.1.is_request }
}
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
if let dm = lookup(pubkey) {
return dm
}
let new = DirectMessageModel()
let new = DirectMessageModel(our_pubkey: our_pubkey)
dms.append((pubkey, new))
return new
}

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: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
}

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, load: .from_events(events), damus_state: state)
}
}
}

View File

@@ -18,11 +18,11 @@ class FollowersModel: ObservableObject {
let sub_id: String = UUID().description
let profiles_id: String = UUID().description
var count_display: String {
var count: Int? {
guard let contacts = self.contacts else {
return "?"
return nil
}
return "\(contacts.count)";
return contacts.count
}
init(damus_state: DamusState, target: String) {
@@ -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)
}
@@ -86,7 +82,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):

View File

@@ -60,7 +60,7 @@ class FollowingModel {
switch nev {
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")

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,23 +50,33 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = []
@Published var dms: DirectMessagesModel = DirectMessagesModel()
@Published var events: [NostrEvent] = []
@Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@Published var events = EventHolder()
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: "")
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
@@ -96,6 +109,8 @@ class HomeModel: ObservableObject {
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
case .list:
handle_list_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
@@ -108,9 +123,62 @@ class HomeModel: ObservableObject {
handle_channel_create(ev)
case .channel_meta:
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
}
}
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)
guard zap.target.pubkey == our_pubkey else {
return
}
if !notifications.insert_zap(zap) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
}
}
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -122,6 +190,12 @@ class HomeModel: ObservableObject {
func handle_channel_meta(_ ev: NostrEvent) {
}
func filter_muted() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
}
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -131,7 +205,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])
@@ -151,7 +225,7 @@ class HomeModel: ObservableObject {
guard inner_ev.is_valid else {
return
}
if inner_ev.is_textlike {
handle_text_event(sub_id: sub_id, ev)
}
@@ -167,6 +241,7 @@ class HomeModel: ObservableObject {
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted)
notify(.update_stats, e)
}
}
@@ -176,14 +251,14 @@ class HomeModel: ObservableObject {
return
}
// CHECK SIGS ON THESE
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
}
}
@@ -224,7 +299,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 == .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
@@ -238,10 +313,11 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
let dms = dms.dms.flatMap { $0.1.events }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
}
self.loading = false
@@ -272,7 +348,11 @@ class HomeModel: ObservableObject {
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
@@ -290,7 +370,6 @@ class HomeModel: ObservableObject {
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
])
@@ -300,16 +379,16 @@ class HomeModel: ObservableObject {
var notifications_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 100
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
@@ -333,9 +412,32 @@ class HomeModel: ObservableObject {
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
}
func handle_list_event(_ ev: NostrEvent) {
// we only care about our lists
guard ev.pubkey == damus_state.pubkey else {
return
}
if let mutelist = damus_state.contacts.mutelist {
if ev.created_at <= mutelist.created_at {
return
}
}
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
return
}
guard name.ref_id == "mute" else {
return
}
damus_state.contacts.set_mutelist(ev)
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -347,93 +449,70 @@ class HomeModel: ObservableObject {
return m[kind]
}
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
let last_ev = get_last_event(timeline)
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
new_events = NewEventsBits(prev: new_events, setting: timeline)
}
}
}
func handle_notification(ev: NostrEvent) {
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
return
}
damus_state.events.insert(ev)
if let inner_ev = ev.inner_event {
damus_state.events.insert(inner_ev)
}
if !notifications.insert_event(ev) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
}
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 {
handle_last_event(ev: ev, timeline: .home)
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits
}
return ok
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
return !ev.should_show_event
func insert_home_event(_ ev: NostrEvent) {
if events.insert(ev) {
handle_last_event(ev: ev, timeline: .home)
}
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
if should_hide_event(ev) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if sub_id == home_subid {
let _ = insert_home_event(ev)
insert_home_event(ev)
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
var inserted = false
var found = false
let ours = ev.pubkey == self.damus_state.pubkey
var i = 0
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
} else {
// self dm!?
print("TODO: handle self dm?")
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
for (pk, _) in dms.dms {
if pk == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
$0.created_at < $1.created_at
}
break
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
}
i += 1
self.incoming_dms = []
return
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev])
dms.dms.append((the_pk, model))
}
if inserted {
handle_last_event(ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.sorted { a, b in
if a.1.events.count > 0 && b.1.events.count > 0 {
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
return false
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 = []
}
}
}
@@ -539,10 +618,17 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
}
return
}
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
@@ -572,14 +658,14 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
// 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))
}
@@ -592,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
@@ -640,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)
}
}
@@ -656,4 +743,155 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
}
}
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
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
} else {
// self dm!?
print("TODO: handle self dm?")
}
}
for (pk, _) in dms.dms {
if pk == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
$0.created_at < $1.created_at
}
break
}
i += 1
}
if !found {
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 {
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
}
return new_events
}
/// A helper to determine if we need to notify the user of new events
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
let last_ev = get_last_event(timeline)
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
return NewEventsBits(prev: new_events, setting: timeline)
}
}
return nil
}
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
return true
}
}
return false
}
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return false
}
return ev.should_show_event
}

View File

@@ -0,0 +1,41 @@
//
// LibreTranslateServer.swift
// damus
//
// Created by Terry Yiu on 1/21/23.
//
import Foundation
enum LibreTranslateServer: 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 argosopentech
case terraprint
case vern
case custom
var model: Model {
switch self {
case .argosopentech:
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
case .vern:
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
case .custom:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

View File

@@ -7,6 +7,10 @@
import Foundation
enum CountResult {
case already_counted
case success(Int)
}
class EventCounter {
var counts: [String: Int] = [:]
@@ -14,11 +18,6 @@ class EventCounter {
var our_events: [String: NostrEvent] = [:]
var our_pubkey: String
enum CountResult {
case already_counted
case success(Int)
}
init (our_pubkey: String) {
self.our_pubkey = our_pubkey
}

View File

@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
let block: Block
}
struct Invoice {
let description: String
let amount: Amount
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash:
return ""
}
}
}
enum Block {
@@ -189,24 +206,78 @@ enum Amount: Equatable {
case .any:
return NSLocalizedString("Any", comment: "Any amount of sats")
case .specific(let amt):
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(amt) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
if formattedSats == numberFormatter.string(from: 1) {
return NSLocalizedString("\(formattedSats) sat", comment: "Amount of 1 sat.")
}
return NSLocalizedString("\(formattedSats) sats", comment: "Amount of sats.")
return format_msats(amt)
}
}
}
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
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
let sats = NSNumber(value: (Double(msats) / 1000.0))
if msats >= 1_000_000*1000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if msats >= 1000*1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return sats.stringValue
}
return formatter.string(from: sats) ?? sats.stringValue
}
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
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? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
@@ -216,9 +287,8 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return nil
}
var description = ""
if b11.description != nil {
description = String(cString: b11.description)
guard let description = convert_invoice_description(b11: b11) else {
return nil
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
@@ -229,6 +299,18 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
}
if var deschash = maybe_pointee(b11.description_hash) {
return .description_hash(Data(bytes: &deschash, count: 32))
}
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)

View File

@@ -0,0 +1,18 @@
//
// ListModel.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import Foundation
/*
class MutelistModel: ObservableObject {
let contacts: Contacts
@Published var users: [String]
}
*/

View File

@@ -0,0 +1,32 @@
//
// ReactionGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class EventGroup {
var events: [NostrEvent]
var last_event_at: Int64 {
guard let first = self.events.first else {
return 0
}
return first.created_at
}
init() {
self.events = []
}
init(events: [NostrEvent]) {
self.events = events
}
func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
}
}

View File

@@ -0,0 +1,53 @@
//
// ZapGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class ZapGroup {
var zaps: [Zap]
var msat_total: Int64
var zappers: Set<String>
var last_event_at: Int64 {
guard let first = zaps.first else {
return 0
}
return first.event.created_at
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request.ev }
}
init(zaps: [Zap]) {
self.zaps = zaps
self.msat_total = 0
self.zappers = Set()
}
init() {
self.zaps = []
self.msat_total = 0
self.zappers = Set()
}
func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
}
msat_total += zap.invoice.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
}
return true
}
}

View File

@@ -0,0 +1,294 @@
//
// NotificationsModel.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
enum NotificationItem {
case repost(String, EventGroup)
case reaction(String, EventGroup)
case profile_zap(ZapGroup)
case event_zap(String, ZapGroup)
case reply(NostrEvent)
var id: String {
switch self {
case .repost(let evid, _):
return "repost_" + evid
case .reaction(let evid, _):
return "reaction_" + evid
case .profile_zap:
return "profile_zap"
case .event_zap(let evid, _):
return "event_zap_" + evid
case .reply(let ev):
return "reply_" + ev.id
}
}
var last_event_at: Int64 {
switch self {
case .reaction(_, let evgrp):
return evgrp.last_event_at
case .repost(_, let evgrp):
return evgrp.last_event_at
case .profile_zap(let zapgrp):
return zapgrp.last_event_at
case .event_zap(_, let zapgrp):
return zapgrp.last_event_at
case .reply(let reply):
return reply.created_at
}
}
}
class NotificationsModel: ObservableObject {
var incoming_zaps: [Zap]
var incoming_events: [NostrEvent]
var should_queue: Bool
// mappings from events to
var zaps: [String: ZapGroup]
var profile_zaps: ZapGroup
var reactions: [String: EventGroup]
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
@Published var notifications: [NotificationItem]
init() {
self.zaps = [:]
self.reactions = [:]
self.reposts = [:]
self.replies = []
self.has_reply = Set()
self.should_queue = true
self.incoming_zaps = []
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
for ev in incoming_events {
pks.insert(ev.pubkey)
}
for grp in reposts {
for ev in grp.value.events {
pks.insert(ev.pubkey)
}
}
for ev in replies {
pks.insert(ev.pubkey)
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
}
return Array(pks)
}
func build_notifications() -> [NotificationItem] {
var notifs: [NotificationItem] = []
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
if !profile_zaps.zaps.isEmpty {
notifs.append(.profile_zap(profile_zaps))
}
for el in reposts {
let evid = el.key
let evgrp = el.value
notifs.append(.repost(evid, evgrp))
}
for el in reactions {
let evid = el.key
let evgrp = el.value
notifs.append(.reaction(evid, evgrp))
}
for reply in replies {
notifs.append(.reply(reply))
}
notifs.sort { $0.last_event_at > $1.last_event_at }
return notifs
}
private func insert_repost(_ ev: NostrEvent) -> Bool {
guard let reposted_ev = ev.inner_event else {
return false
}
let id = reposted_ev.id
if let evgrp = self.reposts[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reposts[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_text(_ ev: NostrEvent) -> Bool {
guard !has_reply.contains(ev.id) else {
return false
}
has_reply.insert(ev.id)
replies.append(ev)
return true
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reactions[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
if ev.known_kind == .boost {
return insert_repost(ev)
} else if ev.known_kind == .like {
return insert_reaction(ev)
} else if ev.known_kind == .text {
return insert_text(ev)
}
return false
}
private func insert_zap_immediate(_ zap: Zap) -> Bool {
switch zap.target {
case .note(let notezt):
let id = notezt.note_id
if let zapgrp = self.zaps[notezt.note_id] {
return zapgrp.insert(zap)
} else {
let zapgrp = ZapGroup()
self.zaps[id] = zapgrp
return zapgrp.insert(zap)
}
case .profile:
return profile_zaps.insert(zap)
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
if insert_event_immediate(ev) {
self.notifications = build_notifications()
return true
}
return false
}
func insert_zap(_ zap: Zap) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
if insert_zap_immediate(zap) {
self.notifications = build_notifications()
return true
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false
var count = 0
count = incoming_events.count
incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in reposts {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
}
changed = changed || el.value.zaps.count != count
}
count = replies.count
replies = replies.filter(isIncluded)
changed = changed || replies.count != count
if changed {
self.notifications = build_notifications()
}
}
func flush() -> Bool {
var inserted = false
for zap in incoming_zaps {
inserted = insert_zap_immediate(zap) || inserted
}
for event in incoming_events {
inserted = insert_event_immediate(event) || inserted
}
if inserted {
self.notifications = build_notifications()
}
return inserted
}
}

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,11 +111,13 @@ 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 {
process_metadata_event(profiles: damus.profiles, ev: ev)
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}

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

59
damus/Models/Report.swift Normal file
View File

@@ -0,0 +1,59 @@
//
// Report.swift
// damus
//
// Created by William Casarin on 2023-01-24.
//
import Foundation
enum ReportType: String {
case explicit
case illegal
case spam
case impersonation
}
struct ReportNoteTarget {
let pubkey: String
let note_id: String
}
enum ReportTarget {
case user(String)
case note(ReportNoteTarget)
}
struct Report {
let type: ReportType
let target: ReportTarget
let message: String
}
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
var tags: [[String]]
switch target {
case .user(let pubkey):
tags = [["p", pubkey]]
case .note(let notet):
tags = [["e", notet.note_id], ["p", notet.pubkey]]
}
tags.append(["report", type.rawValue])
return tags
}
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
let kind = 1984
let tags = create_report_tags(target: report.target, type: report.type)
let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
return ev
}

View File

@@ -0,0 +1,15 @@
//
// RepostsModel.swift
// damus
//
// Created by Terry Yiu on 1/22/23.
//
import Foundation
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()
@@ -30,9 +30,15 @@ class SearchHomeModel: ObservableObject {
return filter
}
func filter_muted() {
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) {
@@ -50,14 +56,14 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && ev.should_show_event && !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):
@@ -70,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime
unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state)
}
@@ -92,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
var pubkeys = Set<String>()
func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] {
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
var pubkeys = Set<String>()
for ev in events {
@@ -107,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String
return Array(pubkeys)
}
func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) {
enum PubkeysToLoad {
case from_events([NostrEvent])
case from_keys([String])
}
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events)
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
filter.authors = authors
guard !authors.isEmpty else {
@@ -125,7 +159,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
}
if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
}

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

@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
}
if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
@@ -207,7 +207,7 @@ class ThreadModel: ObservableObject {
}
if sub_id == self.base_subid {
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state)
}
}

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

@@ -6,6 +6,76 @@
//
import Foundation
import Vault
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)
{
return default_wallet
} else {
return .system_default_wallet
}
}
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
}
return LibreTranslateServer(rawValue: server_name)
}
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
}
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
}
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
@@ -26,16 +96,154 @@ class UserSettingsStore: ObservableObject {
}
}
init() {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
self.default_wallet = default_wallet
} else {
default_wallet = .system_default_wallet
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
@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 {
return
}
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
libretranslate_api_key = ""
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
}
}
}
@Published var libretranslate_url: String {
didSet {
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
}
}
@Published var libretranslate_api_key: String {
didSet {
do {
if libretranslate_api_key == "" {
try clearLibreTranslateApiKey()
} else {
try saveLibreTranslateApiKey(libretranslate_api_key)
}
} catch {
// No-op.
}
}
}
init() {
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet(pubkey)
show_wallet_selector = should_show_wallet_selector(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
// 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 {
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 = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} 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 = ""
}
}
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
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 {
var serviceName = "damus"
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")
@@ -53,7 +53,7 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi:
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet Of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet Of Satoshi."), link: "walletofsatoshi:lightning:",
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee:
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
@@ -75,7 +75,7 @@ enum Wallet: String, CaseIterable, Identifiable {
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
case .blixtwallet:
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
appStoreLink: nil, image: "blixt-wallet")
appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
case .river:
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "river://",
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")

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