Compare commits

..

113 Commits

Author SHA1 Message Date
tyiu 6418b4beb0 Add saved drafts to posts, replies, and DMs 2023-02-12 21:26:22 -05: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
tyiu 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
tyiu 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
William Casarin fe444228e6 Cached relay metadata 2023-02-09 15:56:26 -08: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
tyiu c602c754f8 Import translations 2023-02-08 11:47:46 -05:00
tyiu 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
tyiu 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
tyiu 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
tyiu 567303e680 Add DeepL translation integration
Changelog-Added: DeepL translation integration
Closes: #522
2023-02-06 11:51:50 -08:00
tyiu 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
tyiu 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
tyiu 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
tyiu 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
tyiu 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
tyiu 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
tyiu 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
tyiu 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
tyiu 2cd508c4c2 Import ar translations 2023-02-02 09:28:41 -05:00
tyiu 5e0b4583c0 Import de_AT translations 2023-01-31 20:17:21 -05:00
tyiu 4d2a670c72 Import es_419 translations 2023-01-31 20:16:59 -05:00
tyiu 73d17ac708 Import pt_PT translations 2023-01-31 20:16:30 -05:00
tyiu c2e955faa5 Import pl_PL translations 2023-01-31 20:16:07 -05:00
tyiu 58d95a0c15 Import fr_FR translations 2023-01-31 20:15:36 -05:00
tyiu d86a6a9e16 Import nl translations 2023-01-31 20:15:09 -05:00
tyiu 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
158 changed files with 19642 additions and 2826 deletions
+47 -2
View File
@@ -1,3 +1,50 @@
## [1.0.0-15] - 2023-02-10
### Added
- 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)
- Ability to change remote image loading policy (radixrat)
- 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)
- Make app smaller by optimizing pngs (pea-sys)
- Clicking relay numbers now goes to relay config (radixrat)
### Fixed
- Load zaps, likes and reposts when you open a thread (William Casarin)
- 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
@@ -514,5 +561,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+3 -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
+179 -17
View File
@@ -15,6 +15,10 @@
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
@@ -28,8 +32,6 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
@@ -70,6 +72,8 @@
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */; };
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; };
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */; };
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */; };
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */; };
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA63C28FF52D600C48A62 /* bolt11.c */; };
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64028FF553900C48A62 /* hash_u5.c */; };
4C3EA64428FF558100C48A62 /* sha256.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64328FF558100C48A62 /* sha256.c */; };
@@ -86,6 +90,7 @@
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; };
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
@@ -131,7 +136,16 @@
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; };
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; };
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
@@ -154,6 +168,14 @@
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; };
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
@@ -179,6 +201,7 @@
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
@@ -191,6 +214,8 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */
@@ -220,26 +245,42 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5EA10F297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5EA110297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5EA111297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-AT"; path = "de-AT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
@@ -261,8 +302,6 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
@@ -303,6 +342,8 @@
4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeCounter.swift; sourceTree = "<group>"; };
4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; };
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.swift; sourceTree = "<group>"; };
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapEvent.swift; sourceTree = "<group>"; };
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEvent.swift; sourceTree = "<group>"; };
4C3EA63B28FF52D600C48A62 /* bolt11.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bolt11.h; sourceTree = "<group>"; };
4C3EA63C28FF52D600C48A62 /* bolt11.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bolt11.c; sourceTree = "<group>"; };
4C3EA63E28FF54BD00C48A62 /* short_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = short_types.h; sourceTree = "<group>"; };
@@ -348,6 +389,7 @@
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; };
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
@@ -394,7 +436,16 @@
4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; };
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; };
4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; };
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
@@ -419,6 +470,14 @@
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
4CE8795A2996C47A00F758CC /* ZapsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsModel.swift; sourceTree = "<group>"; };
4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; };
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; };
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
@@ -445,6 +504,7 @@
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
@@ -456,6 +516,8 @@
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -597,6 +659,10 @@
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -604,6 +670,8 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
@@ -639,7 +707,6 @@
4C8682862814DE470026224F /* ProfileView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
@@ -649,7 +716,6 @@
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
647D9A8C2968520300A295DE /* SideMenuView.swift */,
@@ -660,6 +726,7 @@
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -687,8 +754,11 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
4C363A8328233689006E126D /* Parser.swift */,
@@ -706,6 +776,10 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -713,9 +787,14 @@
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
4CE879532996BA0000F758CC /* Detail */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -737,6 +816,15 @@
path = Reactions;
sourceTree = "<group>";
};
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup;
children = (
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
);
path = Profile;
sourceTree = "<group>";
};
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
@@ -748,6 +836,8 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -768,6 +858,8 @@
5C513FB9297F72980072348F /* CustomPicker.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -836,6 +928,8 @@
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -849,6 +943,31 @@
path = damusUITests;
sourceTree = "<group>";
};
4CE879492995B58700F758CC /* Relays */ = {
isa = PBXGroup;
children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CE879532996BA0000F758CC /* Detail */ = {
isa = PBXGroup;
children = (
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
);
path = Detail;
sourceTree = "<group>";
};
4CE879562996C44A00F758CC /* Zaps */ = {
isa = PBXGroup;
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CEE2AE62804F57B00AB5EEF /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -985,7 +1104,6 @@
Base,
"es-419",
"en-US",
"de-AT",
"tr-TR",
"fr-FR",
"lv-LV",
@@ -993,6 +1111,11 @@
de,
"pt-PT",
"pl-PL",
ar,
nl,
"zh-CN",
"el-GR",
ja,
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1058,6 +1181,7 @@
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1073,20 +1197,25 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
@@ -1095,6 +1224,7 @@
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
@@ -1105,12 +1235,15 @@
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
@@ -1118,6 +1251,7 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
@@ -1126,13 +1260,16 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
@@ -1144,6 +1281,7 @@
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
@@ -1180,29 +1318,35 @@
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
@@ -1211,7 +1355,9 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1223,7 +1369,9 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
@@ -1261,7 +1409,6 @@
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5EA111297CCF6C00569477 /* de-AT */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
@@ -1269,6 +1416,11 @@
3AB5B86C2986D8A3006599D2 /* de */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AC524F0298C000B00693EBF /* ar */,
3A96D41C298DA94500388A2A /* nl */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1277,7 +1429,6 @@
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3A5EA10F297CCF6C00569477 /* de-AT */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
@@ -1285,6 +1436,11 @@
3AB5B86A2986D8A3006599D2 /* de */,
3AF6336829884C6B0005672A /* pt-PT */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AC524EE298C000B00693EBF /* ar */,
3A96D41A298DA94500388A2A /* nl */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1293,7 +1449,6 @@
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3A5EA110297CCF6C00569477 /* de-AT */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
@@ -1301,6 +1456,11 @@
3AB5B86B2986D8A3006599D2 /* de */,
3AF6336929884C6B0005672A /* pt-PT */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AC524EF298C000B00693EBF /* ar */,
3A96D41B298DA94500388A2A /* nl */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1436,7 +1596,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1444,6 +1604,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1477,7 +1638,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1485,6 +1646,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+81 -59
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
@EnvironmentObject 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)
}
}
+3 -2
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)
}
}
+135
View File
@@ -0,0 +1,135 @@
//
// 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
let size: EventViewKind
@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()
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
}
}
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.
let content = event.get_content(damus_state.keypair.privkey)
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.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)
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
} 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 blocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: blocks, 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, size: .selected)
}
}
+131
View File
@@ -0,0 +1,131 @@
//
// 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 tip_amount = get_default_tip_amount(pubkey: damus_state.pubkey)
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: tip_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 {
ZStack {
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"))
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
.offset(x: 22)
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
.sheet(isPresented: $showing_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)
}
}
+105 -49
View File
@@ -7,13 +7,12 @@
import SwiftUI
import Starscream
import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://relay.snort.social",
"wss://nostr.orangepill.dev",
"wss://offchain.pub",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
@@ -28,12 +27,16 @@ enum Sheets: Identifiable {
case post
case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
}
}
@@ -89,7 +92,6 @@ struct ContentView: View {
@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()
@@ -112,7 +114,7 @@ struct ContentView: View {
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(userSettings: user_settings) {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post
}
}
@@ -139,6 +141,36 @@ struct ContentView: View {
}
}
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("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
}
}
func MainContent(damus: DamusState) -> some View {
VStack {
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
@@ -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()
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
}
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
@@ -172,24 +205,9 @@ struct ContentView: View {
.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)
.toolbar {
ToolbarItem(placement: .principal) {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
}
.ignoresSafeArea(.keyboard)
@@ -198,7 +216,7 @@ struct ContentView: View {
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
} else {
EmptyView()
}
@@ -246,7 +264,7 @@ struct ContentView: View {
if let damus = self.damus_state {
NavigationView {
ZStack {
VStack {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
@@ -254,29 +272,43 @@ struct ContentView: View {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
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("Filter", 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)
@@ -284,10 +316,8 @@ struct ContentView: View {
.padding([.bottom], 8)
}
}
.environmentObject(user_settings)
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
setup_notifications()
}
.sheet(item: $active_sheet) { item in
@@ -298,6 +328,17 @@ struct ContentView: View {
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
case .event(let event):
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
if #available(iOS 16.0, *) {
RelayFilterView(state: damus_state!, timeline: timeline)
.presentationDetents([.height(550)])
.presentationDragIndicator(.visible)
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
}
}
.onOpenURL { url in
@@ -416,6 +457,8 @@ struct ContentView: View {
let post_res = obj.object as! NostrPostResult
switch post_res {
case .post(let post):
//let post = tup.0
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
self.damus_state?.pool.send(.event(new_ev))
@@ -520,6 +563,7 @@ struct ContentView: View {
}
func switch_timeline(_ timeline: Timeline) {
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
if timeline == self.selected_timeline {
@@ -545,21 +589,33 @@ struct ContentView: View {
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in BOOTSTRAP_RELAYS {
add_relay(pool, relay)
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
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_model: home.drafts_model
)
home.damus_state = self.damus_state!
-1
View File
@@ -24,7 +24,6 @@
<string>zeusln</string>
<string>zebedee</string>
<string>lightning</string>
<string>squarecash</string>
<string>phoenix</string>
<string>lnlink</string>
<string>strike</string>
+11 -9
View File
@@ -11,30 +11,32 @@ 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
}
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 {
+7 -1
View File
@@ -18,6 +18,12 @@ 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_model: DraftsModel
var pubkey: String {
return keypair.pubkey
@@ -29,6 +35,6 @@ struct DamusState {
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
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_model: DraftsModel())
}
}
+35
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 }
}
}
+4
View File
@@ -13,6 +13,8 @@ class DirectMessageModel: ObservableObject {
is_request = determine_is_request()
}
}
@Published var draft: String
var is_request: Bool
var our_pubkey: String
@@ -31,11 +33,13 @@ class DirectMessageModel: ObservableObject {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
init(our_pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
}
+13
View File
@@ -0,0 +1,13 @@
//
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
//
import Foundation
class DraftsModel: ObservableObject {
@Published var post: String = ""
@Published var replies = Dictionary<NostrEvent, String>()
}
+1 -5
View File
@@ -51,11 +51,7 @@ class FollowersModel: ObservableObject {
if has_contact.contains(ev.pubkey) {
return
}
process_contact_event(
pool: damus_state.pool,
contacts: damus_state.contacts,
pubkey: damus_state.pubkey, ev: ev
)
process_contact_event(state: damus_state, ev: ev)
contacts?.append(ev.pubkey)
has_contact.insert(ev.pubkey)
}
+135 -21
View File
@@ -52,15 +52,18 @@ class HomeModel: ObservableObject {
@Published var events: [NostrEvent] = []
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
@Published var drafts_model: DraftsModel
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.drafts_model = DraftsModel()
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.drafts_model = DraftsModel()
}
var pool: RelayPool {
@@ -112,9 +115,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 !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
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: damus_state.pubkey) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
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
@@ -141,7 +197,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])
@@ -234,7 +290,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 == .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
@@ -317,6 +373,7 @@ class HomeModel: ObservableObject {
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 100
@@ -411,7 +468,7 @@ class HomeModel: ObservableObject {
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -423,7 +480,7 @@ class HomeModel: ObservableObject {
}
func handle_dm(_ ev: NostrEvent) {
if let notifs = handle_incoming_dm(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
if let notifs = handle_incoming_dm(contacts: damus_state.contacts, prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
}
}
@@ -590,31 +647,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
@@ -638,14 +695,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)
}
}
@@ -654,7 +712,62 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
}
}
func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
let relay_id = url.absoluteString
guard metadatas.lookup(relay_id: relay_id) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
DispatchQueue.main.async {
metadatas.insert(relay_id: relay_id, metadata: meta)
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
guard let url = URL(string: urlString) else {
return nil
}
var request = URLRequest(url: url)
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
var res: (Data, URLResponse)? = nil
res = try await URLSession.shared.data(for: request)
guard let data = res?.0 else {
return nil
}
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
return nip11
}
func process_relay_metadata() {
}
func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
// hide blocked users
guard should_show_event(contacts: contacts, ev: ev) else {
return prev_events
}
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
@@ -728,9 +841,10 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
}
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return true
return false
}
return !ev.should_show_event
return ev.should_show_event
}
-3
View File
@@ -17,7 +17,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var url: String?
}
case none
case argosopentech
case terraprint
case vern
@@ -25,8 +24,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
case .argosopentech:
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:
+4 -5
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
}
+74 -16
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,20 +206,50 @@ 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
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
return format_msats(amt)
}
}
}
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) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
@@ -212,9 +259,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
@@ -225,6 +271,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)
+19 -1
View File
@@ -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 {
+4 -3
View File
@@ -31,12 +31,13 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
}
func subscribe() {
loading = true
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
}
func unsubscribe(to: String? = nil) {
@@ -54,7 +55,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}
+26 -1
View File
@@ -15,14 +15,20 @@ class SearchModel: ObservableObject {
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 = self.events.filter { should_show_event(contacts: contacts, ev: $0) }
}
func subscribe() {
// since 1 month
search.limit = self.limit
@@ -47,6 +53,10 @@ class SearchModel: ObservableObject {
return
}
guard should_show_event(contacts: contacts, ev: ev) else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
objectWillChange.send()
}
@@ -88,6 +98,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)
+37
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 }
}
}
+150 -23
View File
@@ -8,6 +8,68 @@
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)"
}
let tip_amount_key = "default_tip_amount"
func set_default_tip_amount(pubkey: String, amount: Int64) {
let key = pk_setting_key(pubkey, key: tip_amount_key)
UserDefaults.standard.setValue(amount, forKey: key)
}
func get_default_tip_amount(pubkey: String) -> Int64 {
let key = "\(pubkey)_\(tip_amount_key)"
return UserDefaults.standard.object(forKey: key) as? Int64 ?? 1000000
}
func get_default_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 {
didSet {
@@ -27,6 +89,32 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
}
@Published var deepl_plan: DeepLPlan {
didSet {
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
}
}
@Published var deepl_api_key: String {
didSet {
do {
if deepl_api_key == "" {
try clearDeepLApiKey()
} else {
try saveDeepLApiKey(deepl_api_key)
}
} catch {
// No-op.
}
}
}
@Published var libretranslate_server: LibreTranslateServer {
didSet {
if oldValue == libretranslate_server {
@@ -37,7 +125,7 @@ class UserSettingsStore: ObservableObject {
libretranslate_api_key = ""
if libretranslate_server == .custom || libretranslate_server == .none {
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
@@ -66,46 +154,79 @@ 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
}
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
// 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
if let translationServerName = UserDefaults.standard.string(forKey: "libretranslate_server"),
let translationServer = LibreTranslateServer(rawValue: translationServerName) {
self.libretranslate_server = translationServer
libretranslate_url = translationServer.model.url ?? UserDefaults.standard.object(forKey: "libretranslate_url") as? String ?? ""
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
if let translation_service = get_translation_service(pubkey) {
self.translation_service = translation_service
} else {
// Note from @tyiu:
// Default server is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
libretranslate_server = .none
libretranslate_url = ""
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 = ""
}
}
func saveLibreTranslateApiKey(_ apiKey: String) throws {
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
func clearLibreTranslateApiKey() throws {
private func clearLibreTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
private func saveDeepLApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
}
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
@@ -113,3 +234,9 @@ struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
var accessGroup: String? = nil
var accountName = "libretranslate_apikey"
}
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "deepl_apikey"
}
+1 -1
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")
+77
View File
@@ -0,0 +1,77 @@
//
// ZapsModel.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import Foundation
class ZapsModel: ObservableObject {
let state: DamusState
let target: ZapTarget
var zaps: [Zap]
let zaps_subid = UUID().description
init(state: DamusState, target: ZapTarget) {
self.state = state
self.target = target
self.zaps = []
}
func subscribe() {
var filter = NostrFilter.filter_kinds([9735])
switch target {
case .profile(let profile_id):
filter.pubkeys = [profile_id]
case .note(let note_target):
filter.referenced_ids = [note_target.note_id]
}
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: zaps_subid)
}
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
guard case .nostr_event(let resp) = conn_ev else {
return
}
guard resp.subid == zaps_subid else {
return
}
guard case .event(_, let ev) = resp else {
return
}
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
}
}
}
+5 -1
View File
@@ -102,7 +102,7 @@ struct Profile: Codable {
}
var lnurl: String? {
guard let addr = lud06 ?? lud16 else {
guard let addr = lud16 ?? lud06 else {
return nil;
}
@@ -110,6 +110,10 @@ struct Profile: Codable {
return lnaddress_to_lnurl(addr);
}
if !addr.lowercased().hasPrefix("lnurl") {
return nil
}
return addr;
}
+28 -1
View File
@@ -11,6 +11,8 @@ import secp256k1
import secp256k1_implementation
import CryptoKit
enum ValidationResult: Decodable {
case ok
case bad_id
@@ -79,7 +81,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
var too_big: Bool {
return self.content.count > 32000
return self.content.count > 16000
}
var should_show_event: Bool {
@@ -368,9 +370,14 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
func encode_json<T: Encodable>(_ val: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
}
func decode_json<T: Decodable>(_ val: String) -> T? {
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
}
@@ -571,6 +578,26 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
return ev
}
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
switch target {
case .profile(let pk):
return [["p", pk]]
case .note(let note_target):
return [["e", note_target.note_id], ["p", note_target.author]]
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag)
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
return ev
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
+1
View File
@@ -20,4 +20,5 @@ enum NostrKind: Int {
case channel_meta = 41
case chat = 42
case list = 30000
case zap = 9735
}
+9
View File
@@ -12,11 +12,20 @@ import UIKit
class Profiles {
var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:]
var zappers: [String: String] = [:]
func is_validated(_ pk: String) -> NIP05? {
return validated[pk]
}
func lookup_zapper(pubkey: String) -> String? {
if let zapper = zappers[pubkey] {
return zapper
}
return nil
}
func add(id: String, profile: TimestampedProfile) {
profiles[id] = profile
}
+28 -4
View File
@@ -7,16 +7,16 @@
import Foundation
struct RelayInfo: Codable {
public struct RelayInfo: Codable {
let read: Bool
let write: Bool
static let rw = RelayInfo(read: true, write: true)
}
struct RelayDescriptor: Codable {
let url: URL
let info: RelayInfo
public struct RelayDescriptor: Codable {
public let url: URL
public let info: RelayInfo
}
enum RelayFlags: Int {
@@ -24,6 +24,30 @@ enum RelayFlags: Int {
case broken = 1
}
struct Limitations: Codable {
let payment_required: Bool?
static var empty: Limitations {
Limitations(payment_required: nil)
}
}
struct RelayMetadata: Codable {
let name: String?
let description: String?
let pubkey: String?
let contact: String?
let supported_nips: [Int]?
let software: String?
let version: String?
let limitation: Limitations?
let payments_url: String?
var is_paid: Bool {
return limitation?.payment_required ?? false
}
}
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
+3 -4
View File
@@ -118,10 +118,9 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
}
func make_nostr_push_event(ev: NostrEvent) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
let event_data = try! encoder.encode(ev)
let event = String(decoding: event_data, as: UTF8.self)
guard let event = encode_json(ev) else {
return nil
}
let encoded = "[\"EVENT\",\(event)]"
print(encoded)
return encoded
+21 -2
View File
@@ -42,6 +42,8 @@ class RelayPool {
var relays: [Relay] = []
var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = []
var seen: Set<String> = Set()
var counts: [String: UInt64] = [:]
var descriptors: [RelayDescriptor] {
relays.map { $0.descriptor }
@@ -149,9 +151,9 @@ class RelayPool {
self.send(.unsubscribe(sub_id), to: to)
}
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) {
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
register_handler(sub_id: sub_id, handler: handler)
send(.subscribe(.init(filters: filters, sub_id: sub_id)))
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
}
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
@@ -241,8 +243,25 @@ class RelayPool {
}
}
func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev {
let k = relay_id + nev.id
if !seen.contains(k) {
seen.insert(k)
if counts[relay_id] == nil {
counts[relay_id] = 1
} else {
counts[relay_id] = (counts[relay_id] ?? 0) + 1
}
}
}
}
}
func handle_event(relay_id: String, event: NostrConnectionEvent) {
record_last_pong(relay_id: relay_id, event: event)
record_seen(relay_id: relay_id, event: event)
// run req queue when we reconnect
if case .ws_event(let ws) = event {
+1 -1
View File
@@ -147,7 +147,7 @@ public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
guard let strBytes = str.data(using: .utf8) else {
throw Bech32Error.nonUTF8String
}
guard strBytes.count <= 90 else {
guard strBytes.count <= 2024 else {
throw Bech32Error.stringLengthExceeded
}
var lower: Bool = false
+1 -1
View File
@@ -29,7 +29,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
}
func end_editing() {
public func end_editing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
+20
View File
@@ -38,6 +38,26 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true
}
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
var i: Int = 0
for zap in zaps {
// don't insert duplicate events
if new_zap.event.id == zap.event.id {
return false
}
if new_zap.invoice.amount > zap.invoice.amount {
zaps.insert(new_zap, at: i)
return true
}
i += 1
}
zaps.append(new_zap)
return true
}
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0
+14
View File
@@ -158,6 +158,20 @@ func get_saved_privkey() -> String? {
return mkey.map { $0.trimmingCharacters(in: .whitespaces) }
}
/**
Detects whether a string might contain an nsec1 prefixed private key.
It does not determine if it's the current user's private key and does not verify if it is properly encoded or has the right length.
*/
func contentContainsPrivateKey(_ content: String) -> Bool {
if #available(iOS 16.0, *) {
return content.contains(/nsec1[02-9ac-z]+/)
} else {
let regex = try! NSRegularExpression(pattern: "nsec1[02-9ac-z]+")
return (regex.firstMatch(in: content, range: NSRange(location: 0, length: content.count)) != nil)
}
}
fileprivate func removePrivateKeyFromUserDefaults() throws {
guard let privKey = UserDefaults.standard.string(forKey: "privkey") else { return }
try save_privkey(privkey: privKey)
+24
View File
@@ -0,0 +1,24 @@
//
// LNUrl.swift
// damus
//
// Created by William Casarin on 2023-01-16.
//
import Foundation
struct LNUrlPayRequest: Decodable {
let allowsNostr: Bool?
let nostrPubkey: String?
let minSendable: Int64?
let maxSendable: Int64?
let status: String?
let callback: String?
}
struct LNUrlPayResponse: Decodable {
let pr: String
}
+20
View File
@@ -0,0 +1,20 @@
//
// LNUrls.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import Foundation
class LNUrls {
var endpoints: [String: LNUrlPayRequest]
init() {
self.endpoints = [:]
}
func lookup(_ id: String) -> LNUrlPayRequest? {
return self.endpoints[id]
}
}
+26
View File
@@ -0,0 +1,26 @@
//
// NIPURLBuilder.swift
// damus
//
// Created by Honk on 2/1/23.
//
import Foundation
struct NIPURLBuilder {
static private let baseURL = "https://github.com/nostr-protocol/nips/blob/master/"
static func url(forNIP nip: Int) -> URL? {
let urlString = baseURL + "\(formatNipNumber(nip: nip)).md"
return URL(string: urlString)
}
static func formatNipNumber(nip: Int) -> String {
let formatted: String
if nip < 10 {
formatted = "0\(nip)"
} else {
formatted = "\(nip)"
}
return formatted
}
}
+94
View File
@@ -0,0 +1,94 @@
//
// RelayFilters.swift
// damus
//
// Created by William Casarin on 2023-02-08.
//
import Foundation
struct RelayFilter: Hashable {
let timeline: Timeline
let relay_id: String
init(timeline: Timeline, relay_id: String, on: Bool = false) {
self.timeline = timeline
self.relay_id = relay_id
}
}
class RelayFilters {
private let our_pubkey: String
private var disabled: Set<RelayFilter>
func is_filtered(timeline: Timeline, relay_id: String) -> Bool {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
let contains = disabled.contains(filter)
return contains
}
func remove(timeline: Timeline, relay_id: String) {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
if !disabled.contains(filter) {
return
}
disabled.remove(filter)
save_relay_filters(our_pubkey, filters: disabled)
}
func insert(timeline: Timeline, relay_id: String) {
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
if disabled.contains(filter) {
return
}
disabled.insert(filter)
save_relay_filters(our_pubkey, filters: disabled)
}
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
disabled = load_relay_filters(our_pubkey) ?? Set()
}
}
func save_relay_filters(_ pubkey: String, filters: Set<RelayFilter>) {
let key = pk_setting_key(pubkey, key: "relay_filters")
let arr = Array(filters.map { filter in "\(filter.timeline)\t\(filter.relay_id)" })
UserDefaults.standard.set(arr, forKey: key)
}
func relay_filter_setting_key(_ pubkey: String) -> String {
return pk_setting_key(pubkey, key: "relay_filters")
}
func clear_relay_filters(_ pubkey: String) {
let key = relay_filter_setting_key(pubkey)
UserDefaults.standard.removeObject(forKey: key)
}
func load_relay_filters(_ pubkey: String) -> Set<RelayFilter>? {
let key = relay_filter_setting_key(pubkey)
guard let filters = UserDefaults.standard.stringArray(forKey: key) else {
return nil
}
return filters.reduce(into: Set()) { s, str in
let parts = str.components(separatedBy: "\t")
guard parts.count == 2 else {
return
}
guard let timeline = Timeline.init(rawValue: parts[0]) else {
return
}
let filter = RelayFilter(timeline: timeline, relay_id: parts[1])
s.insert(filter)
}
}
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] {
return pool.descriptors
.map { $0.url.absoluteString }
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
}
+20
View File
@@ -0,0 +1,20 @@
//
// RelayMetadatas.swift
// damus
//
// Created by William Casarin on 2023-02-09.
//
import Foundation
class RelayMetadatas {
private var metadata: [String: RelayMetadata] = [:]
func lookup(relay_id: String) -> RelayMetadata? {
return metadata[relay_id]
}
func insert(relay_id: String, metadata: RelayMetadata) {
self.metadata[relay_id] = metadata
}
}
+127
View File
@@ -0,0 +1,127 @@
//
// Translator.swift
// damus
//
// Created by Terry Yiu on 2/4/23.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct Translator {
private let userSettingsStore: UserSettingsStore
private let session = URLSession.shared
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(_ userSettingsStore: UserSettingsStore) {
self.userSettingsStore = userSettingsStore
}
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return text
}
}
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
struct RequestBody: Encodable {
let q: String
let source: String
let target: String
let api_key: String?
}
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.libretranslate_api_key)
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
return response.translatedText
}
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
let url = try makeURL(userSettingsStore.deepl_plan.model.url, path: "/v2/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("DeepL-Auth-Key \(userSettingsStore.deepl_api_key)", forHTTPHeaderField: "Authorization")
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translations: [DeepLTranslations]
}
struct DeepLTranslations: Decodable {
let detected_source_language: String
let text: String
}
let response: Response = try await decodedData(for: request)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
guard var components = URLComponents(string: baseUrl) else {
throw URLError(.badURL)
}
components.path = path
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
let data = try await session.data(for: request)
let result = try decoder.decode(Output.self, from: data)
return result
}
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
let onCancel = { task?.cancel() }
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
task = dataTask(with: request) { data, _, error in
guard let data = data else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: data)
}
task?.resume()
}
},
onCancel: { onCancel() }
)
}
}
+323
View File
@@ -0,0 +1,323 @@
//
// Zap.swift
// damus
//
// Created by William Casarin on 2023-01-15.
//
import Foundation
enum ZapSource {
case author(String)
// TODO: anonymous
//case anonymous
}
public struct NoteZapTarget: Equatable {
public let note_id: String
public let author: String
}
public enum ZapTarget: Equatable {
case profile(String)
case note(NoteZapTarget)
public static func note(id: String, author: String) -> ZapTarget {
return .note(NoteZapTarget(note_id: id, author: author))
}
var pubkey: String {
switch self {
case .profile(let pk):
return pk
case .note(let note_target):
return note_target.author
}
}
var id: String {
switch self {
case .note(let note_target):
return note_target.note_id
case .profile(let pk):
return pk
}
}
}
struct ZapRequest {
let ev: NostrEvent
}
struct Zap {
public let event: NostrEvent
public let invoice: ZapInvoice
public let zapper: String /// zap authorizer
public let target: ZapTarget
public let request: ZapRequest
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else {
return nil
}
guard let bolt11_str = event_tag(zap_ev, name: "bolt11") else {
return nil
}
guard let bolt11 = decode_bolt11(bolt11_str) else {
return nil
}
/// Any amount invoices are not allowed
guard let zap_invoice = invoice_to_zap_invoice(bolt11) else {
return nil
}
// Some endpoints don't have this, let's skip the check for now. We're mostly trusting the zapper anyways
/*
guard let preimage = event_tag(zap_ev, name: "preimage") else {
return nil
}
guard preimage_matches_invoice(preimage, inv: zap_invoice) else {
return nil
}
*/
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
return nil
}
guard let zap_req = decode_nostr_event_json(desc) else {
return nil
}
guard let target = determine_zap_target(zap_req) else {
return nil
}
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
}
}
/// Fetches the description from either the invoice, or tags, depending on the type of invoice
func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? {
switch inv_desc {
case .description(let string):
return string
case .description_hash(let deschash):
guard let desc = event_tag(ev, name: "description") else {
return nil
}
guard let data = desc.data(using: .utf8) else {
return nil
}
guard sha256(data) == deschash else {
return nil
}
return desc
}
}
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
guard case .specific(let amt) = invoice.amount else {
return nil
}
return ZapInvoice(description: invoice.description, amount: amt, string: invoice.string, expiry: invoice.expiry, payment_hash: invoice.payment_hash, created_at: invoice.created_at)
}
func preimage_matches_invoice<T>(_ preimage: String, inv: LightningInvoice<T>) -> Bool {
guard let raw_preimage = hex_decode(preimage) else {
return false
}
let hashed = sha256(Data(raw_preimage))
return inv.payment_hash == hashed
}
func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? {
guard let ptag = event_tag(ev, name: "p") else {
return nil
}
if let etag = event_tag(ev, name: "e") {
return ZapTarget.note(id: etag, author: ptag)
}
return .profile(ptag)
}
func decode_bolt11(_ s: String) -> Invoice? {
var bs = blocks()
bs.num_blocks = 0
blocks_init(&bs)
let bytes = s.utf8CString
let _ = bytes.withUnsafeBufferPointer { p in
damus_parse_content(&bs, p.baseAddress)
}
guard bs.num_blocks == 1 else {
blocks_free(&bs)
return nil
}
let block = bs.blocks[0]
guard let converted = convert_block(block, tags: []) else {
blocks_free(&bs)
return nil
}
guard case .invoice(let invoice) = converted else {
blocks_free(&bs)
return nil
}
blocks_free(&bs)
return invoice
}
func event_tag(_ ev: NostrEvent, name: String) -> String? {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == name {
return tag[1]
}
}
return nil
}
func decode_nostr_event_json(_ desc: String) -> NostrEvent? {
let decoder = JSONDecoder()
guard let dat = desc.data(using: .utf8) else {
return nil
}
guard let ev = try? decoder.decode(NostrEvent.self, from: dat) else {
return nil
}
return ev
}
func decode_zap_request(_ desc: String) -> ZapRequest? {
let decoder = JSONDecoder()
guard let jsonData = desc.data(using: .utf8) else {
return nil
}
guard let jsonArray = try? JSONSerialization.jsonObject(with: jsonData) as? [[Any]] else {
return nil
}
for array in jsonArray {
guard array.count == 2 else {
continue
}
let mkey = array.first.flatMap { $0 as? String }
if let key = mkey, key == "application/nostr" {
guard let dat = try? JSONSerialization.data(withJSONObject: array[1], options: []) else {
return nil
}
guard let zap_req = try? decoder.decode(NostrEvent.self, from: dat) else {
return nil
}
guard zap_req.kind == 9734 else {
return nil
}
/// Ensure the signature on the zap request is correct
guard case .ok = validate_event(ev: zap_req) else {
return nil
}
return ZapRequest(ev: zap_req)
}
}
return nil
}
func fetch_zapper_from_lnurl(_ lnurl: String) async -> String? {
guard let endpoint = await fetch_static_payreq(lnurl) else {
return nil
}
guard let allows = endpoint.allowsNostr, allows else {
return nil
}
guard let key = endpoint.nostrPubkey, key.count == 64 else {
return nil
}
return endpoint.nostrPubkey
}
func decode_lnurl(_ lnurl: String) -> URL? {
guard let decoded = try? bech32_decode(lnurl) else {
return nil
}
guard decoded.hrp == "lnurl" else {
return nil
}
guard let url = URL(string: String(decoding: decoded.data, as: UTF8.self)) else {
return nil
}
return url
}
func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
guard let url = decode_lnurl(lnurl) else {
return nil
}
guard let ret = try? await URLSession.shared.data(from: url) else {
return nil
}
let json_str = String(decoding: ret.0, as: UTF8.self)
guard let endpoint: LNUrlPayRequest = decode_json(json_str) else {
return nil
}
return endpoint
}
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, amount: Int64) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil
}
let zappable = payreq.allowsNostr ?? false
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable {
if let json = encode_json(zapreq) {
print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json))
}
}
base_url.queryItems = query
guard let url = base_url.url else {
return nil
}
print("url \(url)")
guard let ret = try? await URLSession.shared.data(from: url) else {
return nil
}
let json_str = String(decoding: ret.0, as: UTF8.self)
guard let result: LNUrlPayResponse = decode_json(json_str) else {
print("fetch_zap_invoice error: \(json_str)")
return nil
}
return result.pr
}
+65
View File
@@ -0,0 +1,65 @@
//
// Zaps.swift
// damus
//
// Created by William Casarin on 2023-01-16.
//
import Foundation
class Zaps {
var zaps: [String: Zap]
let our_pubkey: String
var our_zaps: [String: [Zap]]
var event_counts: [String: Int]
var event_totals: [String: Int64]
init(our_pubkey: String) {
self.zaps = [:]
self.our_pubkey = our_pubkey
self.our_zaps = [:]
self.event_counts = [:]
self.event_totals = [:]
}
func add_zap(zap: Zap) {
if zaps[zap.event.id] != nil {
return
}
self.zaps[zap.event.id] = zap
// record our zaps for an event
if zap.request.ev.pubkey == our_pubkey {
switch zap.target {
case .note(let note_target):
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
} else {
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
}
case .profile(_):
break
}
}
// don't count tips to self. lame.
guard zap.request.ev.pubkey != zap.target.pubkey else {
return
}
let id = zap.target.id
if event_counts[id] == nil {
event_counts[id] = 0
}
if event_totals[id] == nil {
event_totals[id] = 0
}
event_counts[id] = event_counts[id]! + 1
event_totals[id] = event_totals[id]! + zap.invoice.amount
return
}
}
+32 -4
View File
@@ -21,18 +21,33 @@ enum ActionBarSheet: Identifiable {
struct EventActionBar: View {
let damus_state: DamusState
let event: NostrEvent
let test_lnurl: String?
let generator = UIImpactFeedbackGenerator(style: .medium)
// just used for previews
@State var sheet: ActionBarSheet? = nil
@State var confirm_boost: Bool = false
@State var show_share_sheet: Bool = false
@StateObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel, test_lnurl: String? = nil) {
self.damus_state = damus_state
self.event = event
self.test_lnurl = test_lnurl
_bar = StateObject.init(wrappedValue: bar)
}
var lnurl: String? {
test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl
}
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
EventActionButton(img: "bubble.left", col: nil) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
}
Spacer()
ZStack {
@@ -44,12 +59,14 @@ struct EventActionBar: View {
self.confirm_boost = true
}
}
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 18)
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Spacer()
ZStack {
LikeButton(liked: bar.liked) {
if bar.liked {
@@ -64,10 +81,17 @@ struct EventActionBar: View {
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
}
if let lnurl = self.lnurl {
Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
}
Spacer()
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
show_share_sheet = true
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
}
.sheet(isPresented: $show_share_sheet) {
if let note_id = bech32_note_id(event.id) {
@@ -145,6 +169,7 @@ struct LikeButton: View {
Image(liked ? "shaka-full" : "shaka-line")
.foregroundColor(liked ? .accentColor : .gray)
}
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
}
}
@@ -155,10 +180,11 @@ struct EventActionBar_Previews: PreviewProvider {
let ds = test_damus_state()
let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 10, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar_ours = ActionBarModel(likes: 100, boosts: 100, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_tip: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_tip: nil)
let bar = ActionBarModel.empty()
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar)
@@ -168,6 +194,8 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
}
.padding(20)
}
+9 -4
View File
@@ -10,7 +10,8 @@ import SwiftUI
struct EventDetailBar: View {
let state: DamusState
let target: String
@StateObject var bar: ActionBarModel
let target_pk: String
@ObservedObject var bar: ActionBarModel
var body: some View {
HStack {
@@ -28,8 +29,12 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle())
}
if bar.tips > 0 {
Text("\(Text("\(bar.tips)", comment: "Number of tip payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("tips_count", comment: "Part of a larger sentence to describe how many tip payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.")
if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
.buttonStyle(PlainButtonStyle())
}
}
}
@@ -37,6 +42,6 @@ struct EventDetailBar: View {
struct EventDetailBar_Previews: PreviewProvider {
static var previews: some View {
EventDetailBar(state: test_damus_state(), target: "", bar: ActionBarModel.empty())
EventDetailBar(state: test_damus_state(), target: "", target_pk: "", bar: ActionBarModel.empty())
}
}
+1
View File
@@ -55,6 +55,7 @@ struct CarouselItemView: View {
.font(.title2)
.foregroundColor(Color.white)
.padding([.leading,.trailing], 50.0)
.minimumScaleFactor(0.5)
}
}
}
+9 -2
View File
@@ -96,17 +96,24 @@ struct ChatView: View {
if let ref_id = thread.replies.lookup(event.id) {
if !is_reply_to_prev() {
ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
/*
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
.frame(maxHeight: expand_reply ? nil : 100)
.environmentObject(thread)
.onTapGesture {
expand_reply = !expand_reply
}
*/
ReplyDescription
}
}
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey), artifacts: .just_content(event.content), size: .normal)
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state,
event: event,
show_images: show_images,
artifacts: .just_content(event.content),
size: .normal)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event, damus: damus_state)
+169 -37
View File
@@ -7,6 +7,7 @@
import AVFoundation
import Kingfisher
import SwiftUI
import LocalAuthentication
struct ConfigView: View {
let state: DamusState
@@ -14,27 +15,62 @@ struct ConfigView: View {
@State var confirm_logout: Bool = false
@State var confirm_delete_account: Bool = false
@State var show_privkey: Bool = false
@State var show_libretranslate_api_key: Bool = false
@State var has_authenticated_locally: Bool = false
@State var show_api_key: Bool = false
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var delete_text: String = ""
@EnvironmentObject var user_settings: UserSettingsStore
@ObservedObject var settings: UserSettingsStore
let generator = UIImpactFeedbackGenerator(style: .light)
init(state: DamusState) {
self.state = state
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_settings = ObservedObject(initialValue: state.settings)
}
func authenticateLocally(completion: @escaping (Bool) -> Void) {
// Need to authenticate only once while ConfigView is presented
guard !has_authenticated_locally else {
completion(true)
return
}
let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
DispatchQueue.main.async {
has_authenticated_locally = success
completion(success)
}
}
} else {
// If there's no authentication set up on the device, let the user copy the key without it
has_authenticated_locally = true
completion(true)
}
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
self.privkey_copied = !is_pk
self.pubkey_copied = is_pk
generator.impactOccurred()
let copyKey = {
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
self.privkey_copied = !is_pk
self.pubkey_copied = is_pk
generator.impactOccurred()
}
if has_authenticated_locally {
copyKey()
} else {
authenticateLocally { success in
if success {
copyKey()
}
}
}
}) {
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
@@ -56,7 +92,7 @@ struct ConfigView: View {
if let sec = state.keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
if show_privkey == false {
if show_privkey == false || !has_authenticated_locally {
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
.disabled(true)
} else {
@@ -68,13 +104,20 @@ struct ConfigView: View {
}
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
.onChange(of: show_privkey) { newValue in
if newValue {
authenticateLocally { success in
show_privkey = success
}
}
}
}
}
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $user_settings.show_wallet_selector).toggleStyle(.switch)
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
selection: $user_settings.default_wallet) {
selection: $settings.default_wallet) {
ForEach(Wallet.allCases, id: \.self) { wallet in
Text(wallet.model.displayName)
.tag(wallet.model.tag)
@@ -82,41 +125,55 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("LibreTranslate Translations", comment: "Section title for selecting the server that hosts the LibreTranslate machine translation API.")) {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $user_settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if user_settings.libretranslate_server != .none {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(user_settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
if show_libretranslate_api_key {
TextField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_libretranslate_api_key = false
}
} else {
SecureField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
Button(NSLocalizedString("Show API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_libretranslate_api_key = true
}
if settings.translation_service == .libretranslate {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.libretranslate_server == .custom {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
}
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .libretranslate)
.autocapitalization(UITextAutocapitalizationType.none)
}
if settings.translation_service == .deepl {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .deepl)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $user_settings.left_handed)
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.toggleStyle(.switch)
}
@@ -139,8 +196,8 @@ struct ConfigView: View {
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
TextField("Type DELETE to delete", text: $delete_text)
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
TextField(NSLocalizedString("Type DELETE to delete", comment: "Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should."), text: $delete_text)
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
confirm_delete_account = false
}
@@ -172,6 +229,81 @@ struct ConfigView: View {
dismiss()
}
}
var libretranslate_view: some View {
VStack {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
if show_api_key {
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_api_key = false
}
}
} else {
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
show_api_key = true
}
}
}
}
}
}
var deepl_view: some View {
VStack {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
HStack {
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
if show_api_key {
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
show_api_key = false
}
}
} else {
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
show_api_key = true
}
}
}
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
}
}
struct ConfigView_Previews: PreviewProvider {
+29 -7
View File
@@ -11,7 +11,7 @@ struct DMChatView: View {
let damus_state: DamusState
let pubkey: String
@EnvironmentObject var dms: DirectMessageModel
@State var message: String = ""
@State var showPrivateKeyWarning: Bool = false
var Messages: some View {
ScrollViewReader { scroller in
@@ -51,7 +51,7 @@ struct DMChatView: View {
}
var InputField: some View {
TextEditor(text: $message)
TextEditor(text: $dms.draft)
.textEditorBackground {
InputBackground()
}
@@ -92,8 +92,17 @@ struct DMChatView: View {
HStack(spacing: 0) {
InputField
if !message.isEmpty {
Button(role: .none, action: send_message) {
if !dms.draft.isEmpty {
Button(
role: .none,
action: {
showPrivateKeyWarning = contentContainsPrivateKey(dms.draft)
if !showPrivateKeyWarning {
send_message()
}
}
) {
Label("", systemImage: "arrow.right.circle")
.font(.title)
}
@@ -102,7 +111,7 @@ struct DMChatView: View {
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
Text(message).opacity(0).padding(.all, 8)
Text(dms.draft).opacity(0).padding(.all, 8)
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
}
@@ -112,7 +121,7 @@ struct DMChatView: View {
func send_message() {
let tags = [["p", pubkey]]
let post_blocks = parse_post_blocks(content: message)
let post_blocks = parse_post_blocks(content: dms.draft)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let content = render_blocks(blocks: post_tags.blocks)
@@ -121,7 +130,7 @@ struct DMChatView: View {
return
}
message = ""
dms.draft = ""
damus_state.pool.send(.event(dm))
end_editing()
@@ -147,6 +156,19 @@ struct DMChatView: View {
}
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
.toolbar { Header }
.onDisappear {
if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
dms.draft = ""
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
send_message()
}
})
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ struct DMView: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
.foregroundColor(is_ours ? Color.white : Color.primary)
.padding(10)
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
+1 -1
View File
@@ -43,6 +43,7 @@ struct DirectMessagesView: View {
}
}
}
.padding(.horizontal)
}
}
@@ -82,7 +83,6 @@ struct DirectMessagesView: View {
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.padding(.horizontal)
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
}
}
+2 -265
View File
@@ -7,119 +7,10 @@
import SwiftUI
struct CollapsedEvents: Identifiable {
let count: Int
let start: Int
let end: Int
var id: String = UUID().description
}
enum CollapsedEvent: Identifiable {
case event(NostrEvent, Highlight)
case collapsed(CollapsedEvents)
var id: String {
switch self {
case .event(let ev, _):
return ev.id
case .collapsed(let c):
return c.id
}
}
}
struct EventDetailView: View {
let sub_id = UUID().description
let damus: DamusState
@StateObject var thread: ThreadModel
@State var collapsed: Bool = true
func toggle_collapse_thread(scroller: ScrollViewProxy, id mid: String?, animate: Bool = true) {
self.collapsed = !self.collapsed
if let id = mid {
if !self.collapsed {
scroll_to_event(scroller: scroller, id: id, delay: 0.1, animate: animate)
}
}
}
func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents)
{
let ev = thread.events[c.start]
print("uncollapsing section at \(c.start) '\(ev.content.prefix(12))...'")
let start_id = ev.id
toggle_collapse_thread(scroller: scroller, id: start_id, animate: false)
}
func CollapsedEventView(_ cev: CollapsedEvent, scroller: ScrollViewProxy) -> some View {
Group {
switch cev {
case .collapsed(let c):
Text(String(format: NSLocalizedString("collapsed_event_view_other_notes", comment: "Text to indicate that the thread was collapsed and that there are other notes to view if tapped."), c.count))
.padding([.top,.bottom], 8)
.font(.footnote)
.foregroundColor(.gray)
.onTapGesture {
//self.uncollapse_section(scroller: proxy, c: c)
//self.toggle_collapse_thread(scroller: proxy, id: nil)
if let ev = thread.events[safe: c.start] {
thread.set_active_event(ev, privkey: damus.keypair.privkey)
}
toggle_thread_view()
}
case .event(let ev, _):
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
if thread.initial_event.id == ev.id {
toggle_thread_view()
} else {
thread.set_active_event(ev, privkey: damus.keypair.privkey)
}
}
}
}
}
var body: some View {
ScrollViewReader { proxy in
if thread.loading {
ProgressView().progressViewStyle(.circular)
}
ScrollView(.vertical) {
LazyVStack {
let collapsed_events = calculated_collapsed_events(
privkey: damus.keypair.privkey,
collapsed: self.collapsed,
active: thread.event,
events: thread.events
)
ForEach(collapsed_events, id: \.id) { cev in
CollapsedEventView(cev, scroller: proxy)
}
}
.padding(.horizontal)
.padding(.top)
EndBlock()
}
.onChange(of: thread.loading) { val in
scroll_after_load(thread: thread, proxy: proxy)
}
.onAppear() {
scroll_after_load(thread: thread, proxy: proxy)
}
}
.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
}
func toggle_thread_view() {
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
Text("EventDetailView")
}
}
@@ -130,167 +21,13 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
}
}
struct EventDetailView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let tm = ThreadModel(evid: "4da698ceac09a16cdb439276fa3d13ef8f6620ffb45d11b76b3f103483c2d0b0", damus_state: state)
EventDetailView(damus: state, thread: tm)
EventDetailView()
}
}
/// Find the entire reply path for the active event
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
{
let event_map: [String: Int] = zip(events,0...events.count).reduce(into: [:]) { (acc, arg1) in
let (ev, i) = arg1
acc[ev.id] = i
}
var is_reply: [String: ()] = [:]
var i: Int = 0
var start: Int = 0
var iterations: Int = 0
if events.count == 0 {
return is_reply
}
for ev in events {
/// does this event reply to the active event?
let ev_refs = ev.event_refs(privkey)
for ev_ref in ev_refs {
if let reply = ev_ref.is_reply {
if reply.ref_id == active.id {
is_reply[ev.id] = ()
start = i
}
}
}
/// does the active event reply to this event?
let active_refs = active.event_refs(privkey)
for active_ref in active_refs {
if let reply = active_ref.is_reply {
if reply.ref_id == ev.id {
is_reply[ev.id] = ()
start = i
}
}
}
i += 1
}
i = start
while true {
if iterations > 1024 {
// infinite loop? or super large thread
print("breaking from large reply_map... big thread??")
break
}
let ev = events[i]
let ref_ids = ev.referenced_ids
if ref_ids.count == 0 {
break
}
let ref_id = ref_ids[ref_ids.count-1]
let pubkey = ref_id.ref_id
is_reply[pubkey] = ()
if let mi = event_map[pubkey] {
i = mi
} else {
break
}
iterations += 1
}
return is_reply
}
func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: NostrEvent) -> Highlight
{
if current.id == active.id {
return .main
} else if reply_map[current.id] != nil {
return .reply
} else {
return .none
}
}
func calculated_collapsed_events(privkey: String?, collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] {
var count: Int = 0
guard let active = active else {
return []
}
let reply_map = make_reply_map(active: active, events: events, privkey: privkey)
if !collapsed {
return events.reduce(into: []) { acc, ev in
let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active)
return acc.append(.event(ev, highlight))
}
}
let nevents = events.count
var start: Int = 0
var i: Int = 0
return events.reduce(into: []) { (acc, ev) in
let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active)
switch highlight {
case .none:
if i == 0 {
start = 1
}
count += 1
case .main: fallthrough
case .custom: fallthrough
case .reply:
if count != 0 {
let c = CollapsedEvents(count: count, start: start, end: i)
acc.append(.collapsed(c))
start = i
count = 0
}
acc.append(.event(ev, highlight))
}
if i == nevents-1 {
if count != 0 {
let c = CollapsedEvents(count: count, start: i-count, end: i)
acc.append(.collapsed(c))
count = 0
}
}
i += 1
}
}
func any_collapsed(_ evs: [CollapsedEvent]) -> Bool {
for ev in evs {
switch ev {
case .collapsed:
return true
case .event:
continue
}
}
return false
}
func print_event(_ ev: NostrEvent) {
print(ev.description)
+33 -71
View File
@@ -57,80 +57,40 @@ struct EventView: View {
}
var body: some View {
return Group {
if event.known_kind == .boost, let inner_ev = event.inner_event {
VStack(alignment: .leading) {
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
VStack {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
VStack(alignment: .leading) {
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
}
.buttonStyle(PlainButtonStyle())
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey)
.padding([.top], 1)
}
.buttonStyle(PlainButtonStyle())
TextEvent(inner_ev, pubkey: inner_ev.pubkey, booster_pubkey: event.pubkey)
.padding([.top], 1)
} else {
EmptyView()
}
} else if event.known_kind == .zap {
if let zap = damus.zaps.zaps[event.id] {
ZapEvent(damus: damus, zap: zap)
} else {
EmptyView()
}
} else {
TextEvent(event, pubkey: pubkey)
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
.padding([.top], 6)
}
Divider()
.padding([.top], 4)
}
}
func TextEvent(_ event: NostrEvent, pubkey: String, booster_pubkey: String? = nil) -> some View {
return HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
}
Spacer()
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text("\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
Spacer()
}
EventBody(damus_state: damus, event: event, size: .normal)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
Rectangle().frame(height: 2).opacity(0)
let bar = make_actionbar_model(ev: event, damus: damus)
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4)
}
Divider()
.padding([.top], 4)
}
.padding([.leading], 2)
}
.contentShape(Rectangle())
.background(event_validity_color(event.validity))
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
}
}
// blame the porn bots for this code
@@ -197,17 +157,19 @@ func format_date(_ created_at: Int64) -> String {
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
let likes = damus.likes.counts[ev.id]
let boosts = damus.boosts.counts[ev.id]
let tips = damus.tips.tips[ev.id]
let zaps = damus.zaps.event_counts[ev.id]
let zap_total = damus.zaps.event_totals[ev.id]
let our_like = damus.likes.our_events[ev.id]
let our_boost = damus.boosts.our_events[ev.id]
let our_tip = damus.tips.our_tips[ev.id]
let our_zap = damus.zaps.our_zaps[ev.id]
return ActionBarModel(likes: likes ?? 0,
boosts: boosts ?? 0,
tips: tips ?? 0,
zaps: zaps ?? 0,
zap_total: zap_total ?? 0,
our_like: our_like,
our_boost: our_boost,
our_tip: our_tip
our_zap: our_zap?.first
)
}
+18 -11
View File
@@ -31,23 +31,30 @@ struct BuilderEventView: View {
return
}
// Is current event
if id == subscription_uuid {
if event != nil {
return
}
event = nostr_event
unsubscribe()
guard id == subscription_uuid else {
return
}
guard nostr_event.known_kind == .text else {
return
}
if event != nil {
return
}
event = nostr_event
unsubscribe()
}
func load() {
subscribe(filters: [
NostrFilter(ids: [self.event_id], limit: 1),
NostrFilter(
ids: [self.event_id],
limit: 1
kinds: [NostrKind.zap.rawValue],
referenced_ids: [self.event_id],
limit: 500
)
])
}
+1 -1
View File
@@ -23,7 +23,7 @@ struct EventBody: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(content), size: size)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(content), size: size)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+2 -2
View File
@@ -25,11 +25,11 @@ struct MutedEventView: View {
self.selected = selected
self._nav_target = nav_target
self._navigating = navigating
self._shown = State(initialValue: !should_hide_event(contacts: damus_state.contacts, ev: event))
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
}
var should_mute: Bool {
return should_hide_event(contacts: damus_state.contacts, ev: event)
return !should_show_event(contacts: damus_state.contacts, ev: event)
}
var FillColor: Color {
+1 -1
View File
@@ -38,7 +38,7 @@ struct SelectedEventView: View {
let bar = make_actionbar_model(ev: event, damus: damus)
if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, bar: bar)
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey, bar: bar)
Divider()
}
+72
View File
@@ -0,0 +1,72 @@
//
// TextEvent.swift
// damus
//
// Created by William Casarin on 2023-02-03.
//
import SwiftUI
struct TextEvent: View {
let damus: DamusState
let event: NostrEvent
let pubkey: String
let has_action_bar: Bool
let booster_pubkey: String?
var body: some View {
HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
}
Spacer()
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text("\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
Spacer()
}
EventBody(damus_state: damus, event: event, size: .normal)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
Rectangle().frame(height: 2).opacity(0)
let bar = make_actionbar_model(ev: event, damus: damus)
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4)
}
}
.padding([.leading], 2)
}
.contentShape(Rectangle())
.background(event_validity_color(event.validity))
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
}
}
struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// ZapEvent.swift
// damus
//
// Created by William Casarin on 2023-02-03.
//
import SwiftUI
struct ZapEvent: View {
let damus: DamusState
let zap: Zap
var body: some View {
VStack(alignment: .leading) {
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
.font(.headline)
.padding([.top], 2)
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil)
.padding([.top], 1)
}
}
}
/*
struct ZapEvent_Previews: PreviewProvider {
static var previews: some View {
ZapEvent()
}
}
*/
+9 -5
View File
@@ -12,13 +12,14 @@ struct FollowButtonView: View {
@Environment(\.colorScheme) var colorScheme
let target: FollowTarget
let follows_you: Bool
@State var follow_state: FollowState
var body: some View {
Button {
follow_state = perform_follow_btn_action(follow_state, target: target)
} label: {
Text(follow_btn_txt(follow_state))
Text(follow_btn_txt(follow_state, follows_you: follows_you))
.frame(width: 105, height: 30)
//.padding(.vertical, 10)
.font(.caption.weight(.bold))
@@ -70,16 +71,19 @@ struct FollowButtonPreviews: View {
var body: some View {
VStack {
Text("Unfollows", comment: "Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.")
FollowButtonView(target: target, follow_state: .unfollows)
FollowButtonView(target: target, follows_you: false, follow_state: .unfollows)
Text("Following", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.")
FollowButtonView(target: target, follow_state: .following)
FollowButtonView(target: target, follows_you: false, follow_state: .following)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
FollowButtonView(target: target, follows_you: false, follow_state: .follows)
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
FollowButtonView(target: target, follow_state: .follows)
FollowButtonView(target: target, follows_you: true, follow_state: .follows)
Text("Unfollowing", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.")
FollowButtonView(target: target, follow_state: .unfollowing)
FollowButtonView(target: target, follows_you: false, follow_state: .unfollowing)
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ struct FollowUserView: View {
HStack {
UserView(damus_state: damus_state, pubkey: target.pubkey)
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
}
}
+1
View File
@@ -275,6 +275,7 @@ struct KeyInput: View {
.autocapitalization(.none)
.foregroundColor(.white)
.font(.body.monospaced())
.textContentType(.password)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
import SwiftUI
enum Timeline: String, CustomStringConvertible {
enum Timeline: String, CustomStringConvertible, Hashable {
case home
case notifications
case search
+159 -312
View File
@@ -9,9 +9,165 @@ import SwiftUI
import LinkPresentation
import NaturalLanguage
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct NoteContentView: View {
let damus_state: DamusState
let event: NostrEvent
let show_images: Bool
@State var artifacts: NoteArtifacts
let size: EventViewKind
@State var preview: LinkViewRepresentable? = nil
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
if size == .selected {
TranslateView(damus_state: damus_state, event: event, size: size)
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
.blur(radius: 10)
.overlay {
Rectangle()
.opacity(0.50)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
if let preview = self.preview, show_images {
preview
} else {
ForEach(artifacts.links, id:\.self) { link in
if let url = link {
LinkViewRepresentable(meta: .url(url))
.frame(height: 50)
}
}
}
}
}
var body: some View {
MainContent()
.onAppear() {
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey)
for block in blocks {
switch block {
case .mention(let m):
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
case .text: return
case .hashtag: return
case .url: return
case .invoice: return
}
}
}
.task {
if let preview = damus_state.previews.lookup(self.event.id) {
switch preview {
case .value(let view):
self.preview = view
case .failed:
// don't try to refetch meta if we've failed
return
}
}
if show_images, artifacts.links.count == 1 {
let meta = await getMetaData(for: artifacts.links.first!)
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
damus_state.previews.store(evid: self.event.id, preview: view)
self.preview = view
}
}
}
func getMetaData(for url: URL) async -> LPLinkMetadata? {
// iOS 15 is crashing for some reason
guard #available(iOS 16, *) else {
return nil
}
let provider = LPMetadataProvider()
do {
return try await provider.startFetchingMetadata(for: url)
} catch {
return nil
}
}
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk)
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
}
}
struct NoteContentView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, artifacts: artifacts, size: .normal)
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
}
}
struct NoteArtifacts {
let content: AttributedString
@@ -65,312 +221,3 @@ func is_image_url(_ url: URL) -> Bool {
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
}
struct NoteContentView: View {
let privkey: String?
let event: NostrEvent
let profiles: Profiles
let previews: PreviewCache
let show_images: Bool
@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
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable? = nil
let size: EventViewKind
@EnvironmentObject var user_settings: UserSettingsStore
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
if size == .selected && noteLanguage != nil && noteLanguage != currentLanguage {
let languageName = Locale.current.localizedString(forLanguageCode: noteLanguage!)
if show_translated_note {
if checkingTranslationStatus {
Button(NSLocalizedString("Translating from \(languageName!)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
show_translated_note = false
}
.font(.footnote)
.contentShape(Rectangle())
.padding(.top, 10)
} else if translated_artifacts != nil {
Button(NSLocalizedString("Translated from \(languageName!)", comment: "Button to indicate that the note has been translated from a different language.")) {
show_translated_note = false
}
.font(.footnote)
.contentShape(Rectangle())
.padding(.top, 10)
Text(translated_artifacts!.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
}
} else {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
}
.font(.footnote)
.contentShape(Rectangle())
.padding(.top, 10)
}
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
.blur(radius: 10)
.overlay {
Rectangle()
.opacity(0.50)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(invoices: artifacts.invoices)
}
if let preview = self.preview, show_images {
preview
} else {
ForEach(artifacts.links, id:\.self) { link in
if let url = link {
LinkViewRepresentable(meta: .url(url))
.frame(height: 50)
}
}
}
}
}
var body: some View {
MainContent()
.onAppear() {
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
}
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(privkey)
for block in blocks {
switch block {
case .mention(let m):
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
}
case .text: return
case .hashtag: return
case .url: return
case .invoice: return
}
}
}
.task {
if let preview = previews.lookup(self.event.id) {
switch preview {
case .value(let view):
self.preview = view
case .failed:
// don't try to refetch meta if we've failed
return
}
}
if show_images, artifacts.links.count == 1 {
let meta = await getMetaData(for: artifacts.links.first!)
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
previews.store(evid: self.event.id, preview: view)
self.preview = view
}
if size == .selected && noteLanguage == nil && !checkingTranslationStatus && user_settings.libretranslate_url != "" {
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.
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: event.content)?.rawValue ?? currentLanguage
if noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: noteLanguage!).identifier(.alpha2)
} else {
noteLanguage = Locale.canonicalLanguageIdentifier(from: noteLanguage!)
}
}
if noteLanguage == nil {
noteLanguage = currentLanguage
translated_note = nil
} else if noteLanguage != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
let translator = Translator(user_settings.libretranslate_url, apiKey: user_settings.libretranslate_api_key)
translated_note = try await translator.translate(event.content, from: noteLanguage!, to: currentLanguage)
} 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 translated_note != nil {
// Render translated note.
let blocks = event.get_blocks(content: translated_note!)
translated_artifacts = render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
}
checkingTranslationStatus = false
}
}
}
func getMetaData(for url: URL) async -> LPLinkMetadata? {
// iOS 15 is crashing for some reason
guard #available(iOS 16, *) else {
return nil
}
let provider = LPMetadataProvider()
do {
return try await provider.startFetchingMetadata(for: url)
} catch {
return nil
}
}
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk)
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
}
}
public struct Translator {
private let url: String
private let apiKey: String?
private let session = URLSession.shared
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
public init(_ url: String, apiKey: String? = nil) {
self.url = url
self.apiKey = apiKey
}
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String {
let url = try makeURL(path: "/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
struct RequestBody: Encodable {
let q: String
let source: String
let target: String
let api_key: String?
}
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: apiKey)
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
return response.translatedText
}
private func makeURL(path: String) throws -> URL {
guard var components = URLComponents(string: url) else {
throw URLError(.badURL)
}
components.path = path
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
let data = try await session.data(for: request)
let result = try decoder.decode(Output.self, from: data)
return result
}
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
let onCancel = { task?.cancel() }
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
task = dataTask(with: request) { data, _, error in
guard let data = data else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: data)
}
task?.resume()
}
},
onCancel: { onCancel() }
)
}
}
struct NoteContentView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal)
}
}
+1 -2
View File
@@ -34,8 +34,7 @@ func PostButton(action: @escaping () -> ()) -> some View {
.keyboardShortcut("n", modifiers: [.command, .shift])
}
func PostButtonContainer(userSettings: UserSettingsStore, action: @escaping () -> Void) -> some View {
let is_left_handed = userSettings.left_handed.self
func PostButtonContainer(is_left_handed: Bool, action: @escaping () -> Void) -> some View {
return VStack {
Spacer()
+45 -1
View File
@@ -16,7 +16,9 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: String = ""
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
let replying_to: NostrEvent?
let references: [ReferencedId]
@@ -46,6 +48,13 @@ struct PostView: View {
let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
if replying_to == nil {
damus_state.drafts_model.post = ""
} else {
damus_state.drafts_model.replies.removeValue(forKey: replying_to!)
}
dismiss()
}
@@ -65,7 +74,11 @@ struct PostView: View {
if !is_post_empty {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
self.send_post()
showPrivateKeyWarning = contentContainsPrivateKey(self.post)
if !showPrivateKeyWarning {
self.send_post()
}
}
}
}
@@ -75,6 +88,13 @@ struct PostView: View {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
if replying_to == nil {
damus_state.drafts_model.post = post
} else {
damus_state.drafts_model.replies[replying_to!] = post
}
}
if post.isEmpty {
Text(POST_PLACEHOLDER)
@@ -94,11 +114,35 @@ struct PostView: View {
}
}
.onAppear() {
if replying_to == nil {
post = damus_state.drafts_model.post
} else {
if damus_state.drafts_model.replies[replying_to!] == nil {
damus_state.drafts_model.replies[replying_to!] = ""
}
post = damus_state.drafts_model.replies[replying_to!]!
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focus = true
}
}
.onDisappear {
if replying_to == nil && damus_state.drafts_model.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts_model.post = ""
} else if replying_to != nil && damus_state.drafts_model.replies[replying_to!]?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true {
damus_state.drafts_model.replies.removeValue(forKey: replying_to!)
}
}
.padding()
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
}
+4 -2
View File
@@ -57,8 +57,10 @@ struct UserSearch_Previews: PreviewProvider {
}
func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] {
func search_users(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
var seen_user = Set<String>()
let search = _search.lowercased()
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
@@ -77,7 +79,7 @@ func search_users(profiles: Profiles, tags: [[String]], search: String) -> [Sear
let profile = profiles.lookup(id: pubkey)
guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else {
guard ((petname?.lowercased().hasPrefix(search) ?? false) || (profile?.name?.lowercased().hasPrefix(search) ?? false)) else {
return
}
+34
View File
@@ -0,0 +1,34 @@
//
// FollowsYou.swift
// damus
//
// Created by William Casarin on 2023-02-07.
//
import SwiftUI
struct FollowsYou: View {
@Environment(\.colorScheme) var colorScheme
var fill_color: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var body: some View {
Text("Follows you", comment: "Text to indicate that a user is following your profile.")
.padding([.leading, .trailing], 6.0)
.padding([.top, .bottom], 2.0)
.foregroundColor(.gray)
.background {
RoundedRectangle(cornerRadius: 5.0)
.foregroundColor(fill_color)
}
.font(.footnote)
}
}
struct FollowsYou_Previews: PreviewProvider {
static var previews: some View {
FollowsYou()
}
}
+61
View File
@@ -0,0 +1,61 @@
//
// ProfileNameView.swift
// damus
//
// Created by William Casarin on 2023-02-07.
//
import SwiftUI
struct ProfileNameView: View {
let pubkey: String
let profile: Profile?
let follows_you: Bool
let damus: DamusState
var spacing: CGFloat { 10.0 }
var body: some View {
Group {
if let real_name = profile?.display_name {
VStack(alignment: .leading) {
Text(real_name)
.font(.title3.weight(.bold))
HStack(alignment: .center, spacing: spacing) {
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
.font(.callout)
.foregroundColor(.gray)
if follows_you {
FollowsYou()
}
}
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
} else {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: spacing) {
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
.font(.title3.weight(.bold))
if follows_you {
FollowsYou()
}
}
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
}
}
}
}
struct ProfileNameView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ProfileNameView(pubkey: test_event.pubkey, profile: nil, follows_you: true, damus: test_damus_state())
ProfileNameView(pubkey: test_event.pubkey, profile: nil, follows_you: false, damus: test_damus_state())
}
}
}
+83 -86
View File
@@ -19,7 +19,7 @@ enum FollowState {
case unfollows
}
func follow_btn_txt(_ fs: FollowState) -> String {
func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String {
switch fs {
case .follows:
return NSLocalizedString("Unfollow", comment: "Button to unfollow a user.")
@@ -28,7 +28,11 @@ func follow_btn_txt(_ fs: FollowState) -> String {
case .unfollowing:
return NSLocalizedString("Unfollowing...", comment: "Label to indicate that the user is in the process of unfollowing another user.")
case .unfollows:
return NSLocalizedString("Follow", comment: "Button to follow a user.")
if follows_you {
return NSLocalizedString("Follow Back", comment: "Button to follow a user back.")
} else {
return NSLocalizedString("Follow", comment: "Button to follow a user.")
}
}
}
@@ -45,35 +49,6 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
}
}
struct ProfileNameView: View {
let pubkey: String
let profile: Profile?
let damus: DamusState
var body: some View {
Group {
if let real_name = profile?.display_name {
VStack(alignment: .leading) {
Text(real_name)
.font(.title3.weight(.bold))
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
.font(.callout)
.foregroundColor(.gray)
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
} else {
VStack(alignment: .leading) {
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
.font(.title3.weight(.bold))
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
}
}
}
}
struct EditButton: View {
let damus_state: DamusState
@@ -92,6 +67,7 @@ struct EditButton: View {
.stroke(borderColor(), lineWidth: 1)
}
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
@@ -116,7 +92,6 @@ struct ProfileView: View {
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var action_sheet_presented: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@@ -142,10 +117,10 @@ struct ProfileView: View {
func LNButton(lnurl: String, profile: Profile) -> some View {
Button(action: {
if user_settings.show_wallet_selector {
if damus_state.settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: lnurl)
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
}
}) {
Image(systemName: "bolt.circle")
@@ -161,8 +136,7 @@ struct ProfileView: View {
}
.cornerRadius(24)
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: lnurl)
.environmentObject(user_settings)
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
}
}
@@ -240,6 +214,61 @@ struct ProfileView: View {
return 0
}
func ActionSection(profile_data: Profile?) -> some View {
return Group {
ActionSheetButton
if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" {
LNButton(lnurl: lnurl, profile: profile)
}
}
DMButton
if profile.pubkey != damus_state.pubkey {
FollowButtonView(
target: profile.get_follow_target(),
follows_you: profile.follows(pubkey: damus_state.pubkey),
follow_state: damus_state.contacts.follow_state(profile.pubkey)
)
} else if damus_state.keypair.privkey != nil {
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
EditButton(damus_state: damus_state)
}
}
}
}
func NameSection(profile_data: Profile?) -> some View {
return Group {
HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.onTapGesture {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
ActionSection(profile_data: profile_data)
.offset(y: -15.0) // Increase if set a frame
}
let follows_you = profile.follows(pubkey: damus_state.pubkey)
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
//.padding(.bottom)
.padding(.top,-(pfp_size/2.0))
}
}
var pfp_size: CGFloat {
return 90.0
}
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geometry in
@@ -252,55 +281,14 @@ struct ProfileView: View {
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading, spacing: 8.0) {
let data = damus_state.profiles.lookup(id: profile.pubkey)
let pfp_size: CGFloat = 90.0
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.onTapGesture {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
Group {
ActionSheetButton
if let profile = data {
if let lnurl = profile.lnurl, lnurl != "" {
LNButton(lnurl: lnurl, profile: profile)
}
}
DMButton
if profile.pubkey != damus_state.pubkey {
FollowButtonView(
target: profile.get_follow_target(),
follow_state: damus_state.contacts.follow_state(profile.pubkey)
)
} else if damus_state.keypair.privkey != nil {
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
EditButton(damus_state: damus_state)
}
}
}
.offset(y: -15.0) // Increase if set a frame
}
ProfileNameView(pubkey: profile.pubkey, profile: data, damus: damus_state)
//.padding(.bottom)
.padding(.top,-(pfp_size/2.0))
NameSection(profile_data: profile_data)
Text(ProfileView.markdown.process(data?.about ?? ""))
.font(.subheadline)
Text(ProfileView.markdown.process(profile_data?.about ?? ""))
.font(.subheadline).textSelection(.enabled)
if let url = data?.website_url {
if let url = profile_data?.website_url {
WebsiteLink(url: url)
}
@@ -334,10 +322,19 @@ struct ProfileView: View {
}
if let relays = profile.relays {
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
}
.buttonStyle(PlainButtonStyle())
} else {
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
}
.buttonStyle(PlainButtonStyle())
}
}
}
@@ -409,7 +406,7 @@ struct ProfileView_Previews: PreviewProvider {
func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache())
let damus = DamusState.empty
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)
+50 -16
View File
@@ -23,19 +23,44 @@ struct QRCodeView: View {
}
var body: some View {
ZStack(alignment: .topLeading) {
DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
ZStack(alignment: .center) {
ZStack(alignment: .topLeading) {
DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
}
.zIndex(1)
VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(Color("DamusWhite"), 4.0), profiles: damus_state.profiles)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.foregroundColor(Color("DamusWhite"))
.padding(.top, 50)
}
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(Color("DamusWhite"))
.font(.body)
}
Spacer()
@@ -46,15 +71,24 @@ struct QRCodeView: View {
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
Text(key)
.font(.headline)
.foregroundColor(Color(.white))
.padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color("DamusWhite"), lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 18, weight: .ultraLight))
Spacer()
}
}
+44
View File
@@ -0,0 +1,44 @@
//
// RelayFilterView.swift
// damus
//
// Created by Ben Weeks on 1/8/23.
//
import SwiftUI
struct RelayFilterView: View {
let state: DamusState
let timeline: Timeline
//@State var relays: [RelayDescriptor]
//@EnvironmentObject var user_settings: UserSettingsStore
//@State var relays: [RelayDescriptor]
init(state: DamusState, timeline: Timeline) {
self.state = state
self.timeline = timeline
//_relays = State(initialValue: state.pool.descriptors)
}
var relays: [RelayDescriptor] {
return state.pool.descriptors
}
var body: some View {
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:")
.padding()
.padding(.top, 20)
.padding(.bottom, 0)
List(Array(relays), id: \.url) { relay in
RelayToggle(state: state, timeline: timeline, relay_id: relay.url.absoluteString)
}
}
}
struct RelayFilterView_Previews: PreviewProvider {
static var previews: some View {
RelayFilterView(state: test_damus_state(), timeline: .search)
}
}
@@ -0,0 +1,33 @@
//
// RelayPaidDetail.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayPaidDetail: View {
let payments_url: String?
@Environment(\.openURL) var openURL
var body: some View {
HStack {
RelayType(is_paid: true)
if let url = payments_url.flatMap({ URL(string: $0) }) {
Button(action: {
openURL(url)
}, label: {
Text("\(url)")
})
}
}
}
}
struct RelayPaidDetail_Previews: PreviewProvider {
static var previews: some View {
RelayPaidDetail(payments_url: "https://jb55.com")
}
}
@@ -37,7 +37,7 @@ struct RecommendedRelayView: View {
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev_after_add)
process_contact_event(state: damus, ev: ev_after_add)
damus.pool.send(.event(ev_after_add))
}
}
+6 -1
View File
@@ -13,6 +13,8 @@ struct RelayConfigView: View {
@State var show_add_relay: Bool = false
@State var relays: [RelayDescriptor]
@Environment(\.dismiss) var dismiss
init(state: DamusState) {
self.state = state
_relays = State(initialValue: state.pool.descriptors)
@@ -33,6 +35,9 @@ struct RelayConfigView: View {
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.descriptors
}
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
@@ -67,7 +72,7 @@ struct RelayConfigView: View {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
process_contact_event(state: state, ev: ev)
state.pool.send(.event(new_ev))
}
+96
View File
@@ -0,0 +1,96 @@
//
// RelayDetailView.swift
// damus
//
// Created by Joel Klabo on 2/1/23.
//
import SwiftUI
struct RelayDetailView: View {
let state: DamusState
let relay: String
let nip11: RelayMetadata
@State private var errorString: String?
@Environment(\.dismiss) var dismiss
func FieldText(_ str: String?) -> some View {
Text(str ?? "No data available")
}
var body: some View {
Group {
Form {
if let pubkey = nip11.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserView(damus_state: state, pubkey: pubkey)
}
}
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
HStack {
Text(relay)
Spacer()
RelayStatus(pool: state.pool, relay: relay)
}
}
if nip11.is_paid {
Section(content: {
RelayPaidDetail(payments_url: nip11.payments_url)
}, header: {
Text("Paid Relay")
}, footer: {
Text("This is a paid relay, you must pay for posts to be accepted.")
})
}
Section(NSLocalizedString("Description", comment: "Label to display relay description.")) {
FieldText(nip11.description)
}
Section(NSLocalizedString("Contact", comment: "Label to display relay contact information.")) {
FieldText(nip11.contact)
}
Section(NSLocalizedString("Software", comment: "Label to display relay software.")) {
FieldText(nip11.software)
}
Section(NSLocalizedString("Version", comment: "Label to display relay software version.")) {
FieldText(nip11.version)
}
if let nips = nip11.supported_nips, nips.count > 0 {
Section(NSLocalizedString("Supported NIPs", comment: "Label to display relay's supported NIPs.")) {
Text(nipsList(nips: nips))
}
}
}
}
.onReceive(handle_notify(.switched_timeline)) { notif in
dismiss()
}
.navigationTitle(nip11.name ?? "")
}
private func nipsList(nips: [Int]) -> AttributedString {
var attrString = AttributedString()
let lastNipIndex = nips.count - 1
for (index, nip) in nips.enumerated() {
if let link = NIPURLBuilder.url(forNIP: nip) {
let nipString = NIPURLBuilder.formatNipNumber(nip: nip)
var nipAttrString = AttributedString(stringLiteral: nipString)
nipAttrString.link = link
attrString = attrString + nipAttrString
if index < lastNipIndex {
attrString = attrString + AttributedString(stringLiteral: ", ")
}
}
}
return attrString
}
}
struct RelayDetailView_Previews: PreviewProvider {
static var previews: some View {
let metadata = RelayMetadata(name: "name", description: "desc", pubkey: "pubkey", contact: "contact", supported_nips: [1,2,3], software: "software", version: "version", limitation: Limitations.empty, payments_url: "https://jb55.com")
RelayDetailView(state: test_damus_state(), relay: "relay", nip11: metadata)
}
}
+50
View File
@@ -0,0 +1,50 @@
//
// RelayStatus.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayStatus: View {
let pool: RelayPool
let relay: String
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
@State var conn_color: Color = .gray
func update_connection_color() {
for relay in pool.relays {
if relay.id == self.relay {
let c = relay.connection
if c.isConnected {
conn_color = .green
} else if c.isConnecting || c.isReconnecting {
conn_color = .yellow
} else {
conn_color = .red
}
}
}
}
var body: some View {
Circle()
.frame(width: 8.0, height: 8.0)
.foregroundColor(conn_color)
.onReceive(timer) { _ in
update_connection_color()
}
.onAppear() {
update_connection_color()
}
}
}
struct RelayStatus_Previews: PreviewProvider {
static var previews: some View {
RelayStatus(pool: test_damus_state().pool, relay: "relay")
}
}
+42
View File
@@ -0,0 +1,42 @@
//
// RelayToggle.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayToggle: View {
let state: DamusState
let timeline: Timeline
let relay_id: String
func toggle_binding(relay_id: String) -> Binding<Bool> {
return Binding(get: {
!state.relay_filters.is_filtered(timeline: timeline, relay_id: relay_id)
}, set: { on in
if !on {
state.relay_filters.insert(timeline: timeline, relay_id: relay_id)
} else {
state.relay_filters.remove(timeline: timeline, relay_id: relay_id)
}
})
}
var body: some View {
HStack {
RelayStatus(pool: state.pool, relay: relay_id)
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false)
Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id))
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
}
struct RelayToggle_Previews: PreviewProvider {
static var previews: some View {
RelayToggle(state: test_damus_state(), timeline: .search, relay_id: "wss://jb55.com")
.padding()
}
}
+27
View File
@@ -0,0 +1,27 @@
//
// RelayType.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct RelayType: View {
let is_paid: Bool
var body: some View {
Image(systemName: is_paid ? "dollarsign.circle.fill" : "globe.americas.fill")
.foregroundColor(is_paid ? Color("DamusGreen") : .gray)
}
}
struct RelayType_Previews: PreviewProvider {
static var previews: some View {
VStack {
RelayType(is_paid: false)
RelayType(is_paid: true)
}
}
}
+14 -29
View File
@@ -11,37 +11,22 @@ struct RelayView: View {
let state: DamusState
let relay: String
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
@State var conn_color: Color = .gray
func update_connection_color() {
for relay in state.pool.relays {
if relay.id == self.relay {
let c = relay.connection
if c.isConnected {
conn_color = .green
} else if c.isConnecting || c.isReconnecting {
conn_color = .yellow
var body: some View {
Group {
HStack {
RelayStatus(pool: state.pool, relay: relay)
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
if let meta = state.relay_metadata.lookup(relay_id: relay) {
NavigationLink {
RelayDetailView(state: state, relay: relay, nip11: meta)
} label: {
Text(relay)
}
} else {
conn_color = .red
Text(relay)
}
}
}
}
var body: some View {
HStack {
Circle()
.frame(width: 8.0, height: 8.0)
.foregroundColor(conn_color)
Text(relay)
}
.onReceive(timer) { _ in
update_connection_color()
}
.onAppear() {
update_connection_color()
}
.swipeActions {
if let privkey = state.keypair.privkey {
RemoveAction(privkey: privkey)
@@ -75,7 +60,7 @@ struct RelayView: View {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: new_ev)
process_contact_event(state: state, ev: new_ev)
state.pool.send(.event(new_ev))
} label: {
Label(NSLocalizedString("Delete", comment: "Button to delete a relay server that the user connects to."), systemImage: "trash")
@@ -86,6 +71,6 @@ struct RelayView: View {
struct RelayView_Previews: PreviewProvider {
static var previews: some View {
RelayView(state: test_damus_state(), relay: "wss://relay.damus.io", conn_color: .red)
RelayView(state: test_damus_state(), relay: "wss://relay.damus.io")
}
}
-65
View File
@@ -1,65 +0,0 @@
//
// SwiftUIView.swift
// damus
//
// Created by William Casarin on 2022-04-19.
//
import SwiftUI
struct ReplyQuoteView: View {
let privkey: String?
let quoter: NostrEvent
let event_id: String
let profiles: Profiles
let previews: PreviewCache
@EnvironmentObject var thread: ThreadModel
func MainContent(event: NostrEvent) -> some View {
HStack(alignment: .top) {
Rectangle()
.frame(width: 2)
.padding([.leading], 4)
.foregroundColor(.accentColor)
VStack(alignment: .leading) {
HStack(alignment: .top) {
ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, profiles: profiles)
Text(Profile.displayName(profile: profiles.lookup(id: event.pubkey), pubkey: event.pubkey))
.foregroundColor(.accentColor)
Text("\(format_relative_time(event.created_at))", comment: "Amount of time that has passed since reply quote event occurred.")
.foregroundColor(.gray)
}
NoteContentView(privkey: privkey, event: event, profiles: profiles, previews: previews, show_images: false, artifacts: .just_content(event.content), size: .normal)
.font(.callout)
.foregroundColor(.accentColor)
//Spacer()
}
//.border(Color.red)
}
//.border(Color.green)
}
var body: some View {
Group {
if let event = thread.lookup(event_id) {
MainContent(event: event)
.padding(4)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
}
}
}
struct ReplyQuoteView_Previews: PreviewProvider {
static var previews: some View {
let s = test_damus_state()
let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey")
ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", profiles: s.profiles, previews: PreviewCache())
.environmentObject(ThreadModel(event: quoter, damus_state: s))
}
}
+23 -4
View File
@@ -15,6 +15,9 @@ struct SaveKeysView: View {
@State var priv_copied: Bool = false
@State var loading: Bool = false
@State var error: String? = nil
@FocusState var pubkey_focused: Bool
@FocusState var privkey_focused: Bool
var body: some View {
ZStack(alignment: .top) {
@@ -39,7 +42,7 @@ struct SaveKeysView: View {
.foregroundColor(.white)
.padding(.bottom, 10)
SaveKeyView(text: account.pubkey_bech32, is_copied: $pub_copied)
SaveKeyView(text: account.pubkey_bech32, textContentType: .username, is_copied: $pub_copied, focus: $pubkey_focused)
.padding(.bottom, 10)
if pub_copied {
@@ -52,7 +55,7 @@ struct SaveKeysView: View {
.foregroundColor(.white)
.padding(.bottom, 10)
SaveKeyView(text: account.privkey_bech32, is_copied: $priv_copied)
SaveKeyView(text: account.privkey_bech32, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
.padding(.bottom, 10)
}
@@ -77,6 +80,13 @@ struct SaveKeysView: View {
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.onAppear {
// Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
pubkey_focused = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
pubkey_focused = false
}
}
}
func complete_account_creation(_ account: CreateAccountModel) {
@@ -138,7 +148,9 @@ struct SaveKeysView: View {
struct SaveKeyView: View {
let text: String
let textContentType: UITextContentType
@Binding var is_copied: Bool
var focus: FocusState<Bool>.Binding
func copy_text() {
UIPasteboard.general.string = text
@@ -166,8 +178,8 @@ struct SaveKeyView: View {
}
}
}
Text(text)
TextField("", text: .constant(text))
.padding(5)
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
@@ -177,7 +189,14 @@ struct SaveKeyView: View {
.foregroundColor(.white)
.onTapGesture {
copy_text()
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
DispatchQueue.main.async {
end_editing()
}
}
.textContentType(textContentType)
.deleteDisabled(true)
.focused(focus)
spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
}
+1 -1
View File
@@ -35,7 +35,7 @@ struct SearchResultsView: View {
}
}
case .hashtag(let ht):
let search_model = SearchModel(pool: damus_state.pool, search: .filter_hashtag([ht]))
let search_model = SearchModel(contacts: damus_state.contacts, pool: damus_state.pool, search: .filter_hashtag([ht]))
let dst = SearchView(appstate: damus_state, search: search_model)
NavigationLink(destination: dst) {
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
+4 -1
View File
@@ -25,6 +25,9 @@ struct SearchView: View {
.onDisappear() {
search.unsubscribe()
}
.onReceive(handle_notify(.new_mutes)) { notif in
search.filter_muted()
}
}
}
@@ -43,7 +46,7 @@ struct SearchView_Previews: PreviewProvider {
let filter = NostrFilter.filter_hashtag(["bitcoin"])
let pool = test_state.pool
let model = SearchModel(pool: pool, search: filter)
let model = SearchModel(contacts: test_state.contacts, pool: pool, search: filter)
SearchView(appstate: test_state, search: model)
}
+3 -3
View File
@@ -9,10 +9,10 @@ import SwiftUI
struct SelectWalletView: View {
@Binding var showingSelectWallet: Bool
let our_pubkey: String
let invoice: String
@Environment(\.openURL) private var openURL
@State var invoice_copied: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@State var allWalletModels: [Wallet.Model] = Wallet.allModels
let generator = UIImpactFeedbackGenerator(style: .light)
@@ -38,7 +38,7 @@ struct SelectWalletView: View {
Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) {
List{
Button() {
let wallet_model = user_settings.default_wallet.model
let wallet_model = get_default_wallet(our_pubkey).model
open_with_wallet(wallet: wallet_model, invoice: invoice)
} label: {
HStack {
@@ -73,6 +73,6 @@ struct SelectWalletView_Previews: PreviewProvider {
@State static var invoice: String = ""
static var previews: some View {
SelectWalletView(showingSelectWallet: $show, invoice: "")
SelectWalletView(showingSelectWallet: $show, our_pubkey: "", invoice: "")
}
}
+75 -88
View File
@@ -10,15 +10,14 @@ import SwiftUI
struct SideMenuView: View {
let damus_state: DamusState
@Binding var isSidebarVisible: Bool
@State var confirm_logout: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme
var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0)
let verticalSpacing: CGFloat = 20
func fillColor() -> Color {
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
@@ -35,104 +34,86 @@ struct SideMenuView: View {
}
.background(Color("DamusDarkGrey").opacity(0.6))
.opacity(isSidebarVisible ? 1 : 0)
.animation(.easeInOut.delay(0.2), value: isSidebarVisible)
.animation(.default, value: isSidebarVisible)
.onTapGesture {
isSidebarVisible.toggle()
}
content
}
.edgesIgnoringSafeArea(.all)
}
var content: some View {
HStack(alignment: .top) {
ZStack(alignment: .top) {
fillColor()
VStack(alignment: .leading, spacing: 20) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey)
let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state)
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
if let picture = damus_state.profiles.lookup(id: damus_state.pubkey)?.picture {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, picture: picture)
} else {
Image(systemName: "person.fill")
VStack(alignment: .leading, spacing: 0) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey)
let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
HStack {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(textColor())
.font(.title)
.lineLimit(1)
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(Color("DamusMediumGrey"))
.font(.body)
.lineLimit(1)
}
}
}
.padding(.bottom, verticalSpacing)
}
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(textColor())
.font(.title)
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(Color("DamusMediumGrey"))
.font(.body)
Divider()
ScrollView {
VStack(spacing: verticalSpacing) {
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
}
/*
NavigationLink(destination: EmptyView()) {
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), systemImage: "bolt")
}
*/
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
navLabel(title: NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon")
}
NavigationLink(destination: RelayConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), systemImage: "network")
}
NavigationLink(destination: ConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
}
}
.padding([.top, .bottom], verticalSpacing)
}
}
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible = false
})
Divider()
.padding(.trailing,40)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
Label(NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
.font(.title2)
.foregroundColor(textColor())
}
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible = false
})
/*
NavigationLink(destination: EmptyView()) {
Label(NSLocalizedString("Relays", comment: "Sidebar menu label for Relay servers view"), systemImage: "xserve")
.font(.title2)
.foregroundColor(textColor())
}
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible.toggle()
})
*/
/*
NavigationLink(destination: EmptyView()) {
Label(NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), systemImage: "bolt")
.font(.title2)
.foregroundColor(textColor())
}
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible.toggle()
})
*/
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
Label(NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon")
.font(.title2)
.foregroundColor(textColor())
}
NavigationLink(destination: RelayConfigView(state: damus_state)) {
Label(NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), systemImage: "network")
.font(.title2)
.foregroundColor(textColor())
}
NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) {
Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
.font(.title2)
.foregroundColor(textColor())
}
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible = false
})
Spacer()
HStack(alignment: .center) {
HStack() {
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
@@ -144,6 +125,7 @@ struct SideMenuView: View {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
})
Spacer()
@@ -154,22 +136,18 @@ struct SideMenuView: View {
Label(NSLocalizedString("", comment: "Sidebar menu label for accessing QRCode view"), systemImage: "qrcode")
.font(.title)
.foregroundColor(textColor())
.padding(.trailing, 20)
}).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state)
}
}
.padding(.top, verticalSpacing)
}
.padding(.top, 60)
.padding(.bottom, 40)
.padding(.leading, 40)
.padding(.top, -15)
.padding([.leading, .trailing, .bottom], 30)
}
.frame(width: sideBarWidth)
.offset(x: isSidebarVisible ? 0 : -sideBarWidth)
.animation(.default, value: isSidebarVisible)
.onTapGesture {
isSidebarVisible.toggle()
}
.alert("Logout", isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
confirm_logout = false
@@ -184,6 +162,15 @@ struct SideMenuView: View {
Spacer()
}
}
@ViewBuilder
func navLabel(title: String, systemImage: String) -> some View {
Label(title, systemImage: systemImage)
.font(.title2)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct Previews_SideMenuView_Previews: PreviewProvider {
+7 -4
View File
@@ -47,6 +47,7 @@ struct BuildThreadV2View: View {
@State var thread: ThreadV2? = nil
@State var current_events_uuid: String = ""
@State var extra_events_uuid: String = ""
@State var childs_events_uuid: String = ""
@State var parents_events_uuids: [String] = []
@@ -197,13 +198,15 @@ struct BuildThreadV2View: View {
self.unsubscribe_all()
print("ThreadV2View: Reload!")
var extra = NostrFilter.filter_kinds([9735, 6, 7])
extra.referenced_ids = [ self.event_id ]
// Get the current event
current_events_uuid = subscribe(filters: [
NostrFilter(
ids: [self.event_id],
limit: 1
)
NostrFilter(ids: [self.event_id], limit: 1)
])
extra_events_uuid = subscribe(filters: [extra])
print("subscribing to threadV2 \(event_id) with sub_id \(current_events_uuid)")
}
-99
View File
@@ -1,99 +0,0 @@
//
// ThreadView.swift
// damus
//
// Created by William Casarin on 2022-04-19.
//
import SwiftUI
struct ThreadView: View {
@StateObject var thread: ThreadModel
let damus: DamusState
@State var is_chatroom: Bool
@State var metadata: ChatroomMetadata? = nil
@State var seen_first: Bool = false
@Environment(\.dismiss) var dismiss
var body: some View {
Group {
if is_chatroom {
ChatroomView(damus: damus)
.navigationBarTitle(metadata?.name ?? NSLocalizedString("Chat", comment: "Navigation bar title for Chatroom view."))
.environmentObject(thread)
} else {
EventDetailView(damus: damus, thread: thread)
.navigationBarTitle(metadata?.name ?? NSLocalizedString("Thread", comment: "Navigation bar title for threaded event detail view."))
.environmentObject(thread)
}
/*
NavigationLink(destination: edv, isActive: $is_chatroom) {
EmptyView()
}
*/
}
.onReceive(handle_notify(.switched_timeline)) { n in
dismiss()
}
.onReceive(handle_notify(.toggle_thread_view)) { _ in
is_chatroom = !is_chatroom
//print("is_chatroom: \(is_chatroom)")
}
.onReceive(handle_notify(.chatroom_meta)) { n in
let meta = n.object as! ChatroomMetadata
self.metadata = meta
}
.onChange(of: thread.events) { val in
if seen_first {
return
}
if let ev = thread.events.first {
guard ev.is_root_event() else {
return
}
seen_first = true
is_chatroom = should_show_chatroom(ev)
}
}
.onAppear() {
thread.subscribe()
}
.onDisappear() {
thread.unsubscribe()
}
}
}
/*
struct ThreadView_Previews: PreviewProvider {
static var previews: some View {
ThreadView()
}
}
*/
func should_show_chatroom(_ ev: NostrEvent) -> Bool {
if ev.known_kind == .chat || ev.known_kind == .channel_create {
return true
}
return has_hashtag(ev.tags, hashtag: "chat")
}
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
}
+42
View File
@@ -0,0 +1,42 @@
//
// ZapsView.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import SwiftUI
struct ZapsView: View {
let state: DamusState
@StateObject var model: ZapsModel
init(state: DamusState, target: ZapTarget) {
self.state = state
self._model = StateObject(wrappedValue: ZapsModel(state: state, target: target))
}
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.zaps, id: \.event.id) { zap in
ZapEvent(damus: state, zap: zap)
.padding()
}
}
}
.navigationBarTitle(NSLocalizedString("Zaps", comment: "Navigation bar title for the Zaps view."))
.onAppear {
model.subscribe()
}
.onDisappear {
model.unsubscribe()
}
}
}
struct ZapsView_Previews: PreviewProvider {
static var previews: some View {
ZapsView(state: test_damus_state(), target: .profile("pk"))
}
}
+9
View File
@@ -0,0 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "دامُس";
/* Bundle name */
"CFBundleName" = "دامُس";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "السماح لدامُس بالوصول إلى الصور يتيح لك حفظ الصور";
+654
View File
@@ -0,0 +1,654 @@
/* Blank space to separate profile picture from profile editor form. */
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "سيتم التحقق من '%@' @ '%@'";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "'%@' عنوان NIP-05 غير صالح. من المفترض أن يشابه صيغة الايميل مثل المثال الموضح.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "متابعي (Profile.displayName(profile: profile, pubkey: whos))";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) يتابع";
/* Prefix character to username. */
"@" = "@";
/* Abbreviated version of a nostr public key. */
"%@" = "%@";
/* Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.
Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "تم حظر %@";
/* 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. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "انشاء حسابك لايتطلب رقم جوال أو بريد الكتروني أو معلومات شخصية. احصل على حسابك الخاص في ثواني.";
/* 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. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "محادثات خاصة مشفرة كليا. ";
/* 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. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." = "%@. بسهولة مطلقة، أرسل و استقبل برقيات البتكوين ⚡️عملة الانترنت العالمية.";
/* Number of zap payments on a post.
Number of relay servers a user is connected. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
"%lld/%lld" = "%lld/%lld";
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Text indicating the zap amount. i.e. number of satoshis that were tipped to a user */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "النبذة التعريفية";
/* Label for About Me section of user profile form. */
"About Me" = "النبذة التعريفية";
/* Placeholder text for About Me description. */
"Absolute Boss" = "مدير كبير";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "موافق";
/* Label to indicate the public ID of the account. */
"Account ID" = "معرف الحساب";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "خيارات";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "اضافة";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "اضافة الجميع";
/* Label for section for adding a relay server. */
"Add Relay" = "اضافة موصّل";
/* Any amount of sats */
"Any" = "أي شيء";
/* Prompt for optional entry of API Key to use translation server. */
"API Key (optional)" = "مفتاح API (اختياري)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "هل أنت متأكد من اعادة النشر؟";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "صورة الخلفية";
/* Reminder to user that they should save their account information. */
"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." = "قبل البدء، يجب عليك حفظ معلومات حسابك لتتمكن من الوصول إليه مستقبلا في حالة حذف دامُس أو تغيير جهازك.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "اكراميات البتكوين";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* Alert button to block a user.
Button to block a profile.
Context menu option for blocking users. */
"Block" = "حظر";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "حظر %@؟";
/* Title of alert for blocking a user. */
"Block User" = "حظر المستخدم";
/* Sidebar menu label for Profile view. */
"Blocked" = "قائمة الحظر";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "المحظورون";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "بث";
/* Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel deleting the user.
Cancel out of logging out the user. */
"Cancel" = "الغاء";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "المحادثة";
/* Button for clearing cached data. */
"Clear" = "مسح";
/* Section title for clearing cached data. */
"Clear Cache" = "مسح البيانات المؤقتة";
/* Label indicating that a user's key was copied. */
"Copied" = "تم النسخ";
/* Button to copy a relay server address. */
"Copy" = "نسخ";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "نسخ عنوان الحساب";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "نسخ الصورة";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "نسخ رابط الصورة";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "نسخ البرقية";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "نسخ LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "نسخ معرف المنشور";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "نسخ المنشور بصيغة JSON";
/* Button to copy report ID. */
"Copy Report ID" = "نسخ معرف البلاغ";
/* Context menu option for copying the text from an note. */
"Copy Text" = "نسخ النص";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User Pubkey" = "نسخ معرف الحساب";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "لم يتم العثور حساب لحظره";
/* Button to create account. */
"Create" = "انشاء";
/* Button to create an account. */
"Create Account" = "انشاء حساب";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "أنشئ قائمة حظر جديدة";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "مبتكر البتكوين. اسطورة لن تتكرر.";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "مخصص";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "دامُس";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "المحفظة الافتراضية";
/* Button for deleting the users account.
Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.
Section title for deleting the user */
"Delete" = "حذف";
/* Alert for deleting the users account.
Button to delete the user's account. */
"Delete Account" = "حذف الحساب";
/* Alert message to indicate this is a deleted account */
"Deleted Account" = "حذف الحساب";
/* Button to dismiss a text field alert. */
"Dismiss" = "اغلاق";
/* Label to prompt display name entry. */
"Display Name" = "الاسم";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "الرسائل الخاصة";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "انهاء";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "اكسب المال.";
/* Button to edit user's profile. */
"Edit" = "تحرير";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "تحرير المشاركين";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "مشفر";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "أدخل مفتاح حسابك لتسجيل الدخول:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "خطأ: %@";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "اتفاقية الاستخدام";
/* Button to follow a user. */
"Follow" = "متابعة";
/* Label describing followers of a user. */
"Followers" = "المتابعون";
/* 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. */
"Following" = "المتابَعين";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "يتابع...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "تابع";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "عام";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "عرض المنشور %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "عرض الحساب %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "اخفاء";
/* Button to hide the DeepL translation API key.
Button to hide the LibreTranslate server API key. */
"Hide API Key" = "اخفاء مفتاح API";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "الرئيسية";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://example.com/pic.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Button for user to report that the account or content has illegal content. */
"Illegal content" = "محتوى غير قانوني";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "المفتاح غير صالح";
/* Button for user to report that the account or content has spam. */
"It's spam" = "سبام";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
/* Moves the post button to the left side of the screen */
"Left Handed" = "تفضيل استخدام اليد اليسرى";
/* Button to complete account creation and start using the app. */
"Let's go!" = "هيا بنا!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "عنوان البرق أو LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "برقية";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "الاختيار";
/* Button to log into account.
Button to log into an account. */
"Login" = "الدخول";
/* Alert for logging out the user.
Button for logging out the user.
Button to close the alert that informs that the current account has been deleted. */
"Logout" = "الخروج";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "تأكد من حفظ مفتاح حسابك السري قبل الخروج حتى لا تفقد امكانية الدخول الى حسابك.";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "تحقق NIP-05";
/* Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key. */
"No" = "لا";
/* Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. */
"No block list found, create a new one? This will overwrite any previous block lists." = "لم نعثر على قائمة حظر. هل تريد انشاء قائمة جديدة؟ سيتم استبدال أي قوائم سابقة ان وجدت";
/* No search results. */
"none" = "لا شيء";
/* Dropdown option for selecting no translation service. */
"None" = "لا اختيار";
/* Alert user that they might be attempting to paste a private key and ask them to confirm. */
"Note contains \"nsec1\" private key. Are you sure?" = "يحتوي المنشور على مفتاح خاص \"nsec1\". هل أنت متأكد؟";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "لا جديد في هذه اللحظة. يرجى المعاودة لاحقا!";
/* Navigation title for notifications. */
"Notifications" = "التنبيهات";
/* String indicating that a given timestamp just occurred */
"now" = "الان";
/* 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. */
"nsec1..." = "nsec1...";
/* Button for user to report that the account or content has nudity or explicit content. */
"Nudity or explicit content" = "عري أو محتوى فاضح";
/* Label indicating that a form input is optional. */
"optional" = "غير الزامي";
/* Button to pay a Lightning invoice. */
"Pay" = "ادفع";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "ادفع البرقية";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "انشر";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "منشور لمستخدم محظور";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "المنشورات";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "المنشورات والردود";
/* Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading. */
"Private" = "خصوصية";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "المفتاح السري";
/* Sidebar menu label for Profile view. */
"Profile" = "الملف الشخصي";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "صورة الحساب";
/* Section title for the user's public account ID. */
"Public Account ID" = "معرف الحساب";
/* Label indicating that the text is a user's public account key. */
"Public key" = "المفتاح العام";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "المفتاح العام";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "مفتاح عام؟";
/* Navigation bar title for Reactions view. */
"Reactions" = "التفاعل";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "موصّلات موصى بها";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "رفض";
/* Label to display relay address.
Text field for relay server. Used for testing purposes. */
"Relay" = "موصّل";
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "موصّلات";
/* Description of what was done as a result of sending a report to relay servers. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "تم ابلاغ الموصّلات وسيتم الاستفادة من هذا البلاغ لتصفية المحتوى. شكرا لك!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "حذف المشاركين";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "رد على منشوره السابق";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "رد على %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "رد على:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "ابلاغ";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "معرف البلاغ";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "تم الابلاغ!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "إعادة نشر";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "منشور مُعاد";
/* Navigation bar title for Reposts view. */
"Reposts" = "اعادات النشر";
/* Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message. */
"Requests" = "طلبات";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "اعادة المحاولة";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "ساتوشي";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "ساتوشي ناكاموتو";
/* Button for saving profile. */
"Save" = "حفظ";
/* Context menu option to save an image. */
"Save Image" = "حفظ الصورة";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "البحث عن وسم: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "بحث...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "المفتاح السري للحساب";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "اختر محفظة البرق";
/* Prompt selection of user's default wallet */
"Select default wallet" = "المحفظة الافتراضية";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "أرسل رسالة لبدء المحادثة...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "خادم";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "الاعدادات";
/* Button to share a post
Button to share an image.
Button to share the link to a profile. */
"Share" = "مشاركة";
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "عرض";
/* Button to show the DeepL translation API key.
Button to show the LibreTranslate server API key. */
"Show API Key" = "عرض مفتاح API";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "هل تريد اختيار المحفظة عند كل عملية دفع؟";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "خروج";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Button to close out of alert that informs that the action to block a user was successful. */
"Thanks!" = "شكرا!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "انتحال صفة شخص آخر";
/* Warning that the inputted account key is a public key and the result of what happens because of it. */
"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." = "هذا مفتاح عام. لن تستطيع النشر أو التفاعل بهذا الحساب بأي طريقة. تستطيع فقط مشاهدة المحتوى العام من منظور صاحب الحساب.";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"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." = "صيغة المفتاح قديمة. لا نستطيع التحديد إذا ما كان المفتاح خاصا أو عاما. الرجاء تفعيل الخانة بالأسفل إذا كان المفتاح عاما.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "هذا معرف حسابك. بإمكانك إرساله لأصدقائك حتى يتمكنوا من متابعتك. اضغط للنسخ.";
/* Label to describe that a private key is the user's secret account key and what they should do with it. */
"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!" = "هذا مفتاح الحساب السري. تحتاجه للدخول إلى حسابك. لا تشاركه مع أي شخص! احتفظ به في مكان آمن مثل برنامج إدارة كلمات المرور السرية. ";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "منشور";
/* Button to translate note from different language. */
"Translate Note" = "ترجم المنشور";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "مُترجَم من (lang)";
/* Button to indicate that the note is in the process of being translated from a different language. */
"Translating from (lang)..." = "الترجمة من (lang)...";
/* Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should. */
"Type DELETE to delete" = "اكتب DELETE لتأكيد الحذف";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "اكتب المنشور هنا...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
/* Button to unfollow a user. */
"Unfollow" = "الغاء المتابعة";
/* 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. */
"Unfollowing" = "يلغي المتابعة";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "يلغي المتابعة...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "ألغى متابعة";
/* Example URL to LibreTranslate server */
"URL" = "رابط";
/* Alert message to indicate the user has been blocked */
"User blocked" = "الحساب محظور";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "تم الحظر";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "اسم المستخدم";
/* Sidebar menu label for Wallet view. */
"Wallet" = "المحفظة";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "تفضيلات المحفظة";
/* Label for Website section of user profile form. */
"Website" = "موقع الكتروني";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "مرحبا بك في شبكتك الاجتماعية!";
/* Text to welcome user. */
"Welcome, %@!" = "مرحبا، %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "عن ماذا تريد الابلاغ";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://some.relay.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "نعم، استبدل";
/* Button to proceed with posting a note even though it looks like they might be posting a private key. */
"Yes, Post with Private Key" = "نعم، انشر المفتاح الخاص";
/* 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. */
"you" = "أنت";
/* Label for Your Name section of user profile form. */
"Your Name" = "الاسم";
/* Footer text to inform user what will happen when the report is submitted. */
"Your report will be sent to the relays you are connected to" = "سيتم ارسال بلاغك للموصّلات المتصلة بحسابك";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
+222
View File
@@ -0,0 +1,222 @@
<?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>collapsed_event_view_other_notes</key>
<dict>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>%d منشورات اضافية</string>
<key>many</key>
<string>%d منشورات اضافية</string>
<key>one</key>
<string>%d منشور اضافي</string>
<key>other</key>
<string>%d منشورات اضافية</string>
<key>two</key>
<string>%d منشوران</string>
<key>zero</key>
<string>%d منشورات أخرى</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
</dict>
<key>followers_count</key>
<dict>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>المتابعون</string>
<key>many</key>
<string>المتابعون</string>
<key>one</key>
<string>متابع</string>
<key>other</key>
<string>المتابعون</string>
<key>two</key>
<string>متابعان</string>
<key>zero</key>
<string>متابع</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>تفاعلات</string>
<key>many</key>
<string>تفاعل</string>
<key>one</key>
<string>تفاعل</string>
<key>other</key>
<string>تفاعل</string>
<key>two</key>
<string>تفاعل</string>
<key>zero</key>
<string>تفاعل</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>موصّلات</string>
<key>many</key>
<string>موصّلات</string>
<key>one</key>
<string> موصّل</string>
<key>other</key>
<string>موصّلات</string>
<key>two</key>
<string>موصّلان</string>
<key>zero</key>
<string>موصّل</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>رد على %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string> &amp; %d آخرون</string>
<key>many</key>
<string> &amp; %d آخرون</string>
<key>one</key>
<string>&amp; %d آخر</string>
<key>other</key>
<string>&amp; %d آخرون</string>
<key>two</key>
<string> &amp; %d آخران</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>رد على%@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string> &amp; %d آخرون</string>
<key>many</key>
<string> &amp; %d آخرون</string>
<key>one</key>
<string>&amp; %d آخر</string>
<key>other</key>
<string>&amp; %d آخرون</string>
<key>two</key>
<string> &amp; %d آخران</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>اعادات نشر</string>
<key>many</key>
<string>اعادات نشر</string>
<key>one</key>
<string>اعادة نشر</string>
<key>other</key>
<string>اعادات نشر</string>
<key>two</key>
<string>اعادات نشر</string>
<key>zero</key>
<string>اعادات نشر</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>few</key>
<string>%2$@ ساتوشي</string>
<key>many</key>
<string>%2$@ ساتوشي</string>
<key>one</key>
<string>%2$@ ساتوشي</string>
<key>other</key>
<string>%2$@ ساتوشي</string>
<key>two</key>
<string>%2$@ ساتوشي</string>
<key>zero</key>
<string>%2$@ ساتوشي</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Zaps</string>
<key>many</key>
<string>Zaps</string>
<key>one</key>
<string>Zap</string>
<key>other</key>
<string>Zaps</string>
<key>two</key>
<string>Zaps</string>
<key>zero</key>
<string>Zaps</string>
</dict>
</dict>
</dict>
</plist>
+1
View File
@@ -7,6 +7,7 @@
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:damus.io</string>
<string>webcredentials:damus.io</string>
</array>
<key>keychain-access-groups</key>
<array>
-1
View File
@@ -7,7 +7,6 @@
import SwiftUI
@main
struct damusApp: App {
var body: some Scene {
+51 -19
View File
@@ -5,7 +5,7 @@
"'%@' at '%@' will be used for verification" = "'%@' bei '%@' wird zur Verifizierung benutzt werden.";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' ist eine ungültige nip05 Kennzeichnung. Diese sollte wie eine Emailadresse aussehen. ";
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "%@' ist kein gülter NIP-05 identifier. Dieser sollte wie eine email aussehen. ";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
@@ -62,7 +62,7 @@ Number of profiles a user is following. */
"Account ID" = "Konto ID";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Aktionen";
"Actions" = "Handlungen";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
@@ -77,6 +77,9 @@ Number of profiles a user is following. */
/* Any amount of sats */
"Any" = "beliebig";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "API Schlüssel (optional)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Bist du sicher dass Du den Beitrag auf deinem Profil teilen möchtest?";
@@ -126,6 +129,7 @@ Number of profiles a user is following. */
Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel deleting the user.
Cancel out of logging out the user. */
"Cancel" = "Abbrechen";
@@ -193,14 +197,19 @@ Number of profiles a user is following. */
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Auswahl";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "Damus";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "Voreingestelltes Wallet";
/* Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist. */
/* Button for deleting the users account.
Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.
Section title for deleting the user */
"Delete" = "Löschen";
/* Button to dismiss a text field alert. */
@@ -209,9 +218,6 @@ Number of profiles a user is following. */
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet. DM is the English abbreviation for Direct Message. */
"DM Type" = "PN Typ";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "PNs";
@@ -240,9 +246,6 @@ Number of profiles a user is following. */
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Button to follow a user. */
"Follow" = "Folgen";
@@ -268,6 +271,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Navigation link to go to profile. */
"Goto profile %@" = "Gehe zum Profil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Verstecken";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "API Schlüssel verstecken";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Heim";
@@ -295,6 +304,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to complete account creation and start using the app. */
"Let's go!" = "Lass uns loslegen!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "LibreTranslate Übersetzungen";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Lightning-Adresse oder LNURL";
@@ -313,7 +325,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
Button to close the alert that informs that the current account has been deleted. */
"Logout" = "Ausloggen";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
@@ -331,6 +343,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* No search results. */
"none" = "keine";
/* Dropdown option for selecting no translation server. */
"None" = "Keine";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Hier gibt es nichts zu sehen. Komm später wieder vorbei!";
@@ -361,6 +376,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to post a note. */
"Post" = "Veröffentlichen";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "Nachricht von einem/e User/in den/die Du geblockt hast";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Beiträge";
@@ -403,7 +421,8 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view */
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "Relays";
/* Description of what was done as a result of sending a report to relay servers. */
@@ -444,9 +463,6 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message. */
"Requests" = "Anfragen";
/* Section title for resetting the user */
"Reset" = "Zurücksetzen";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Erneut versuchen";
@@ -483,6 +499,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sende eine Nachricht um eine Unterhaltung zu beginnen...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Server";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
@@ -491,9 +510,13 @@ Part of a larger sentence to describe how many profiles a user is following. */
Button to share the link to a profile. */
"Share" = "Teilen";
/* Toggle to show or hide user's secret account login key. */
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "Anzeigen";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "API Schlüssel anzeigen";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Wallet-Auswahl zeigen";
@@ -525,6 +548,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
Navigation bar title for threaded event detail view. */
"Thread" = "Thema";
/* Button to translate note from different language. */
"Translate Note" = "Note übersetzen";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (languageName!)" = "Übersetzt aus (languageName!)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Schreibe deinen Beitrag hier...";
@@ -543,7 +572,10 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Entfolgen";
/* Alert message to indicate */
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Benutzer blockiert";
/* Alert message that informs a user was blocked. */
@@ -556,8 +588,8 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Sidebar menu label for Wallet view. */
"Wallet" = "Wallet";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Wallet-Auswahl";
+5 -2
View File
@@ -4,6 +4,9 @@
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Zum Speichern von Bildern braucht Damus Zugriff auf deine Fotos";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "Lokale Authentifizierung für den Zugriff auf den privaten Schlüssel";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Zum Speichern von Bildern braucht Damus Zugriff auf deine Fotos.";
+168 -40
View File
@@ -5,10 +5,10 @@
"'%@' at '%@' will be used for verification" = "'%@' bei '%@' wird zur Verifizierung benutzt werden.";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' ist eine ungültige nip05 Kennung. Diese sollte wie eine Emailadresse aussehen. ";
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "%@' ist kein gülter NIP-05 identifier. Dieser sollte wie eine email aussehen. ";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Follower von (Profile.displayName(profile: profile, pubkey: whos))";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) folgt";
@@ -16,28 +16,27 @@
/* Prefix character to username. */
"@" = "@";
/* Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key. */
/* Abbreviated version of a nostr public key. */
"%@" = "%@";
/* Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.
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'. */
/* Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.
Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "%@ wurde blockiert";
/* 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. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "%@. Du brauchst für ein Konto keine Telefonnummer, Emailadresse oder Namen. Fang ganz reibungslos einfach an.";
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "%@. Du brauchst für ein Konto keine Telefonnummer, E-Mail-Adresse oder Namen. Fang ganz reibungslos einfach an.";
/* 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. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. Ende-zu-Ende verschlüsselter privater Nachrichtenaustausch. Halte Big Tech aus deinen PNs heraus";
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. Ende-zu-Ende-verschlüsselter, privater Nachrichtenaustausch. Halte Big Tech aus deinen PNs heraus.";
/* 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. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." = "%@. Belohne Beiträge deiner Freunde und sammle Sats mit Bitcoin⚡️, der eigenen Währung des Internets.";
/* Number of tip payments on a post.
Number of profiles a user is following. */
/* Number of zap payments on a post.
Number of relay servers a user is connected. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
@@ -46,6 +45,9 @@ Number of profiles a user is following. */
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Text indicating the zap amount. i.e. number of satoshis that were tipped to a user */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Über";
@@ -74,11 +76,20 @@ Number of profiles a user is following. */
/* Label for section for adding a relay server. */
"Add Relay" = "Relay hinzufügen";
/* Label to display relay contact user. */
"Admin" = "Admin";
/* Any amount of sats */
"Any" = "Beliebig";
/* Prompt for optional entry of API Key to use translation server. */
"API Key (optional)" = "API Schlüssel (optional)";
/* Prompt for required entry of API Key to use translation server. */
"API Key (required)" = "API-Schlüssel (benötigt)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Bist du sicher dass Du den Beitrag auf deinem Profil teilen möchtest?";
"Are you sure you want to repost this?" = "Bist du sicher, dass Du den Beitrag teilen möchtest?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Bannerbild";
@@ -115,6 +126,9 @@ Number of profiles a user is following. */
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Accessibility label for boosts button */
"Boosts" = "Boosts";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
@@ -126,6 +140,7 @@ Number of profiles a user is following. */
Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel deleting the user.
Cancel out of logging out the user. */
"Cancel" = "Abbrechen";
@@ -141,6 +156,9 @@ Number of profiles a user is following. */
/* Section title for clearing cached data. */
"Clear Cache" = "Zwischenspeicher löschen";
/* Label to display relay contact information. */
"Contact" = "Kontakt";
/* Label indicating that a user's key was copied. */
"Copied" = "Kopiert";
@@ -193,25 +211,40 @@ Number of profiles a user is following. */
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Anpassen";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "Damus";
/* Dropdown option for selecting DeepL as the translation service. */
"DeepL (Proprietary, Higher Accuracy)" = "DeepL (Proprietär, bessere Genauigkeit)";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "Voreingestellte Wallet";
/* Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist. */
/* Button for deleting the users account.
Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.
Section title for deleting the user */
"Delete" = "Löschen";
/* Alert for deleting the users account.
Button to delete the user's account. */
"Delete Account" = "Konto löschen";
/* Alert message to indicate this is a deleted account */
"Deleted Account" = "Gelöschtes Konto";
/* Label to display relay description. */
"Description" = "Beschreibung";
/* Button to dismiss a text field alert. */
"Dismiss" = "Schließen";
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet. DM is the English abbreviation for Direct Message. */
"DM Type" = "PN Typ";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "PNs";
@@ -240,14 +273,17 @@ Number of profiles a user is following. */
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Button to follow a user back. */
"Follow Back" = "Ebenfalls folgen";
/* Text on QR code view to prompt viewer looking at screen to follow the user. */
"Follow me on nostr" = "Folge mir auf Nostr";
/* Label describing followers of a user. */
"Followers" = "Gefolgte:r";
"Followers" = "Follower";
/* 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. */
@@ -259,6 +295,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Folgt";
/* Text to indicate that a user is following your profile. */
"Follows you" = "Folgt dir";
/* Dropdown option for selecting Free plan for DeepL translation service. */
"Free" = "kostenlos";
/* Button to navigate to DeepL website to get a translation API key. */
"Get API Key" = "API-Schlüssel erhalten";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Allgemein";
@@ -268,6 +313,13 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Navigation link to go to profile. */
"Goto profile %@" = "Gehe zum Profil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Verstecken";
/* Button to hide the DeepL translation API key.
Button to hide the LibreTranslate server API key. */
"Hide API Key" = "API Schlüssel verstecken";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Heim";
@@ -293,7 +345,10 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Left Handed" = "Linkshändig";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Los gehts!";
"Let's go!" = "Los gehts!";
/* Dropdown option for selecting LibreTranslate as the translation service. */
"LibreTranslate (Open Source)" = "LibreTranslate (Open Source)";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Lightning-Adresse oder LNURL";
@@ -301,9 +356,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Lightning-Rechnung";
/* Accessibility Label for Like button */
"Like" = "Like";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Face ID usage description shown when trying to access private key */
"Local authentication to access private key" = "Lokale Authentifizierung für den Zugriff auf den privaten Schlüssel";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "System-Standard";
@@ -313,7 +374,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
Button to close the alert that informs that the current account has been deleted. */
"Logout" = "Ausloggen";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
@@ -325,16 +386,25 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "NIP-05-Verifizierung";
/* Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key. */
"No" = "Nein";
/* Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. */
"No block list found, create a new one? This will overwrite any previous block lists." = "Es wurde keine Blockier-Liste gefunden, soll eine neue erzeugt werden? Dies überschreibt eventuelle frühere Blockier-Listen.";
/* No search results. */
"none" = "keine";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Hier gibts nichts zu sehen. Schau später wieder vorbei!";
/* Dropdown option for selecting no translation service. */
"None" = "Kein";
/* Navigation title for notifications. */
/* Alert user that they might be attempting to paste a private key and ask them to confirm. */
"Note contains \"nsec1\" private key. Are you sure?" = "Notiz enthält einen privaten \"nsec1\"-Schlüssel. Bist du sicher?";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Hier gibt es nichts zu sehen. Schau später wieder vorbei!";
/* Toolbar label for Notifications view. */
"Notifications" = "Benachrichtigungen";
/* String indicating that a given timestamp just occurred */
@@ -358,9 +428,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Prompt selection of DeepL subscription plan to perform machine translations on notes */
"Plan" = "Paket";
/* Button to post a note. */
"Post" = "Teilen";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "Nachricht von einem/e User/in den/die Du geblockt hast.";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Beiträge";
@@ -373,6 +449,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Privater Schlüssel";
/* Dropdown option for selecting Pro plan for DeepL translation service. */
"Pro" = "Pro";
/* Sidebar menu label for Profile view. */
"Profile" = "Profil";
@@ -400,10 +479,11 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Ablehnen";
/* Text field for relay server. Used for testing purposes. */
/* Label to display relay address.
Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view */
/* Sidebar menu label for Relays view. */
"Relays" = "Relays";
/* Description of what was done as a result of sending a report to relay servers. */
@@ -412,8 +492,11 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button label to remove all participants from a note reply. */
"Remove all" = "Alle entfernen";
/* Accessibility label for reply button */
"Reply" = "Antworten";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Antwort an dich selbst";
"Reply to self" = "Antwort an sich selbst";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "%1$@ & %2$@ antworten";
@@ -433,10 +516,10 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Selbst teilen";
"Repost" = "Teilen";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Selbst geteilt";
"Reposted" = "Geteilt";
/* Navigation bar title for Reposts view. */
"Reposts" = "Geteilte Beiträge";
@@ -444,9 +527,6 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message. */
"Requests" = "Anfragen";
/* Section title for resetting the user */
"Reset" = "Zurücksetzen";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Erneut versuchen";
@@ -465,6 +545,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Context menu option to save an image. */
"Save Image" = "Bild sichern";
/* Text on QR code view to prompt viewer to scan the QR code on screen with their device camera. */
"Scan the code" = "Code scannen";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Hashtag suchen: #%@";
@@ -483,26 +566,44 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sende eine Nachricht um eine Unterhaltung zu beginnen...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Server";
/* Prompt selection of translation service provider. */
"Service" = "Dienst";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
/* Button to share an image.
/* Button to share a post
Button to share an image.
Button to share the link to a profile. */
"Share" = "Teilen";
/* Toggle to show or hide user's secret account login key. */
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "Anzeigen";
/* Button to show the DeepL translation API key.
Button to show the LibreTranslate server API key. */
"Show API Key" = "API Schlüssel anzeigen";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Wallet-Auswahl anzeigen";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Abmelden";
/* Label to display relay software. */
"Software" = "Software";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Label to display relay's supported NIPs. */
"Supported NIPs" = "Unterstützte NIPs";
/* Button to close out of alert that informs that the action to block a user was successful. */
"Thanks!" = "Danke!";
@@ -513,7 +614,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"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." = "Dies ist ein öffentlicher Schlüssel, Du wirst keine Beiträge teilen oder oder auf irgendeine Weise interagieren können. Dies wird genutzt um andere Kontos aus deren Perspektive zu sehen.";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"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." = "Dies ist ein nostr-Schlüsse im veralteten Format. Wir sind nicht sicher ob es ein öffentlicher Schlüssel oder ein privater Schlüssel ist. Bitte betätige die untenstehende Schaltfläche wenn es ein öffentlicher Schlüssel ist.";
"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." = "Dies ist ein Nostr-Schlüssel im veralteten Format. Wir sind nicht sicher ob es ein öffentlicher Schlüssel oder ein privater Schlüssel ist. Bitte betätige die untenstehende Schaltfläche wenn es ein öffentlicher Schlüssel ist.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Dies ist deine Konto-ID, die du an deine Freunde weitergeben kannst, damit sie dir folgen können. Zum Kopieren anklicken.";
@@ -525,6 +626,21 @@ Part of a larger sentence to describe how many profiles a user is following. */
Navigation bar title for threaded event detail view. */
"Thread" = "Thema";
/* Button to translate note from different language. */
"Translate Note" = "Notiz übersetzen";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "Übersetzt aus (lang)";
/* Button to indicate that the note is in the process of being translated from a different language. */
"Translating from (lang)..." = "Übersetzung aus (lang)...";
/* Section title for selecting the translation service. */
"Translations" = "Übersetzungen";
/* Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should. */
"Type DELETE to delete" = "Gib DELETE ein, um zu löschen";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Schreibe deinen Beitrag hier...";
@@ -543,7 +659,10 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Entfolgen";
/* Alert message to indicate */
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Benutzer blockiert";
/* Alert message that informs a user was blocked. */
@@ -553,11 +672,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
Label to prompt username entry. */
"Username" = "Benutzername";
/* Label to display relay software version. */
"Version" = "Version";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Wallet";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Wallet-Auswahl";
@@ -566,7 +688,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Website" = "Website";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Willkommen in dem sozialen Netzwerk das %@ kontrolliert.";
"Welcome to the social network %@ control." = "Willkommen in dem sozialen Netzwerk das %@ kontrollierst.";
/* Text to welcome user. */
"Welcome, %@!" = "Willkommen, %@!";
@@ -580,6 +702,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Ja, überschreiben";
/* Button to proceed with posting a note even though it looks like they might be posting a private key. */
"Yes, Post with Private Key" = "Ja, teile mit privatem Schlüssel";
/* 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. */
"you" = "Du";
@@ -589,6 +714,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Footer text to inform user what will happen when the report is submitted. */
"Your report will be sent to the relays you are connected to" = "Die Meldung wird an Relays versendet, mit denen du verbunden bist";
/* Accessibility label for zap button */
"Zap" = "Zap";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
+7 -7
View File
@@ -27,9 +27,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Gefolgte:r</string>
<string>Follower</string>
<key>other</key>
<string>Gefolgte</string>
<string>Follower</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
@@ -134,20 +134,20 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Trinkgeld</string>
<string>Zap</string>
<key>other</key>
<string>Trinkgelder</string>
<string>Zaps</string>
</dict>
</dict>
</dict>

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