Compare commits

..

117 Commits

Author SHA1 Message Date
8de59dea24 Refactor private key detection logic 2023-02-04 16:33:36 -05:00
34b8ba2b52 Add alert to warn against posting nsec1 private keys 2023-02-04 15:56:32 -05:00
Joel Klabo
ac1a5d237e Add Copy Invoice Button
Changelog-Added: Copy invoice button
Closes: #469
2023-02-04 12:52:57 -08:00
William Casarin
cfcd799d63 Fix build 2023-02-04 12:23:24 -08:00
OlegAba
351b32308f Fix DM view padding
Changelog-Fixed: DM Padding
Closes: #476
2023-02-04 12:22:08 -08:00
Hanton Yang
5a4299edaa Fix text truncation in CarouselItemView
Closes: #481
2023-02-04 12:21:21 -08:00
ericholguin
99b619e011 Updated QR Code view, include profile image, name, and remove pubkey text
Closes: #443
Changelog-Changed: Updated QR code view, include profile image, etc
2023-02-04 12:11:20 -08:00
William Casarin
d5ee9e4780 Revert "Lightweight png files"
This reverts commit 71acb16387.
2023-02-04 12:07:42 -08:00
radixrat
ced5b4974f Add remote image loading policy settings
Changelog-Added: Ability to change remote image loading policy
2023-02-04 11:44:41 -08:00
9be55b08fd Fix profile edit button text to not wrap
Closes: #500
2023-02-04 10:39:57 -08:00
Peer Richelsen
ac5f39a922 replace testflight with new app store link
Closes: #503
2023-02-04 10:39:42 -08:00
Joel Klabo
0e9691ae7a Add Test Status Badge
Closes: #512
2023-02-04 10:39:30 -08:00
1441d339a7 Export and import translations, remove de_AT in favor of de, and move zh to zh-CN
Closes: #515
2023-02-04 10:35:32 -08:00
Alex
2517132041 Remove brackets in the image example
Closes: #518
2023-02-04 10:34:56 -08:00
pea-sys
71acb16387 Lightweight png files
Changelog-Changed: Make app smaller by optimizing pngs
Closes: #507
2023-02-04 10:09:33 -08:00
William Casarin
9e2e8595e8 move text event to its own file, improve zaps 2023-02-04 10:01:37 -08:00
radixrat
1a2e9464af add friends of friends, apply to all images 2023-02-03 19:37:23 -05:00
Thomas
63dd39c7e4 Using enum 2023-02-03 19:37:23 -05:00
radixrat
40be9885c5 add remote loading image setting 2023-02-03 19:37:23 -05:00
William Casarin
331d7e9792 make lnurl sanity check case insensitive 2023-02-03 11:41:31 -08:00
William Casarin
d21613a765 removed extra divider 2023-02-03 11:29:45 -08:00
William Casarin
7780120504 ensure lnurls are actually lnurls
Changelog-Fixed: Check for broken lnurls
2023-02-03 11:29:11 -08:00
William Casarin
1696e0365e refactor: settings and translation view 2023-02-03 09:25:07 -08:00
William Casarin
006f8d79e0 Lightning Zaps
Added initial lightning zaps/tipping integration

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

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

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

View File

@@ -1,10 +1,60 @@
## [1.0.0-13] - 2023-01-30
### Added
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
### Changed
- Remove redundant logout button from settings (Jonathan Milligan)
- Moved relay config to its own sidebar entry (William Casarin)
- New stylized tabs (ericholguin)
### Fixed
- Fix hidden profile action sheet when clicking ... (William Casarin)
- Fixed height of DM input (Terry Yiu)
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
## [1.0.0-12] - 2023-01-28
### Added
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
### Changed
- Remove markdown link support from posts (Joel Klabo)
### Fixed
- Fixed crash on some SVG profile pictures (OlegAba)
- Localization fixes
- Don't allow blocking yourself (Terry)
- Hide muted users from global (William Casarin)
- Fixed profiles sometimes not loading from other clients (William Casarin)
- Fixed bug where `spam` was always the report type (William Casarin)
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
## [1.0.0-11] - 2023-01-25
### Added
- Reposts view (Terry Yiu)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (William Casarin)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
@@ -31,7 +81,7 @@
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (William Casarin)
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Add DM Message Requests (William Casarin)
@@ -51,7 +101,7 @@
- Drastically improved image viewer (OlegAba)
- Added pinch to zoom on images (Swift)
- Add Latin American Spanish translations (William Casarin)
- Add Latin American Spanish translations (Nicolás Valencia)
- Added SVG profile picture support (OlegAba)
@@ -465,3 +515,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2

View File

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

View File

@@ -18,6 +18,7 @@
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 */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
@@ -27,7 +28,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 */; };
@@ -69,6 +69,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 */; };
@@ -85,6 +87,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 */; };
@@ -116,6 +119,8 @@
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
@@ -128,7 +133,14 @@
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 */; };
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 */; };
@@ -170,14 +182,17 @@
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE829844AF100D66079 /* AnyCodable.swift */; };
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
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 */; };
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 */; };
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
@@ -219,24 +234,37 @@
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>"; };
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>"; };
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>"; };
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>"; };
3AEABD20297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEABD21297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEABD22297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-DE"; path = "de-DE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -248,7 +276,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>"; };
@@ -290,6 +317,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>"; };
@@ -335,6 +364,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>"; };
@@ -367,6 +397,8 @@
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
@@ -379,7 +411,14 @@
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>"; };
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>"; };
@@ -424,11 +463,16 @@
4CF0ABE829844AF100D66079 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@@ -445,8 +489,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */,
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
@@ -578,6 +620,7 @@
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -585,6 +628,8 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CAAD8AE29888A9B00060CEA /* Relays */,
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
3AA24800297E3DAE0090C62D /* Reposts */,
@@ -618,9 +663,6 @@
4C8682862814DE470026224F /* ProfileView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
@@ -640,6 +682,7 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -668,6 +711,7 @@
isa = PBXGroup;
children = (
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
@@ -683,10 +727,26 @@
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
4CF0ABD72981980C00D66079 /* Lists.swift */,
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
);
path = Util;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
isa = PBXGroup;
children = (
@@ -715,6 +775,8 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -732,8 +794,11 @@
4CB8838C296F710400DC99E7 /* Reposted.swift */,
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
5C513FB9297F72980072348F /* CustomPicker.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -802,6 +867,8 @@
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -841,6 +908,14 @@
path = AnyCodable;
sourceTree = "<group>";
};
4CF0ABF42985CD4200D66079 /* Posting */ = {
isa = PBXGroup;
children = (
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
);
path = Posting;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -870,8 +945,6 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
7C45AE6C297352F90031D7BC /* SVGKit */,
7C45AE6E297352F90031D7BC /* SVGKitSwift */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -945,12 +1018,16 @@
Base,
"es-419",
"en-US",
"de-AT",
"de-DE",
"tr-TR",
"fr-FR",
"lv-LV",
"it-IT",
de,
"pt-PT",
"pl-PL",
ar,
nl,
"zh-CN",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -958,7 +1035,6 @@
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -1012,6 +1088,7 @@
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
@@ -1031,19 +1108,24 @@
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 */,
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 */,
@@ -1062,6 +1144,7 @@
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 */,
@@ -1075,6 +1158,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 */,
@@ -1094,19 +1178,23 @@
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
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 */,
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
@@ -1116,6 +1204,7 @@
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
@@ -1133,8 +1222,8 @@
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 */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
@@ -1147,6 +1236,7 @@
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 */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
@@ -1154,6 +1244,7 @@
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
@@ -1174,7 +1265,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 */,
@@ -1212,12 +1305,16 @@
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5EA111297CCF6C00569477 /* de-AT */,
3AEABD22297CCFA8003F2975 /* de-DE */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AC524F0298C000B00693EBF /* ar */,
3A96D41C298DA94500388A2A /* nl */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1226,12 +1323,16 @@
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3A5EA10F297CCF6C00569477 /* de-AT */,
3AEABD20297CCFA8003F2975 /* de-DE */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
3AF6336829884C6B0005672A /* pt-PT */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AC524EE298C000B00693EBF /* ar */,
3A96D41A298DA94500388A2A /* nl */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1240,12 +1341,16 @@
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3A5EA110297CCF6C00569477 /* de-AT */,
3AEABD21297CCFA8003F2975 /* de-DE */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
3AF6336929884C6B0005672A /* pt-PT */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AC524EF298C000B00693EBF /* ar */,
3A96D41B298DA94500388A2A /* nl */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1381,7 +1486,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1422,7 +1527,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1604,14 +1709,6 @@
minimumVersion = 1.0.0;
};
};
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SVGKit/SVGKit";
requirement = {
kind = revision;
revision = e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1635,16 +1732,6 @@
package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */;
productName = Vault;
};
7C45AE6C297352F90031D7BC /* SVGKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKit;
};
7C45AE6E297352F90031D7BC /* SVGKitSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;

View File

@@ -26,14 +26,6 @@
"version" : "4.0.4"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit",
"state" : {
"revision" : "e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0"
}
},
{
"identity" : "vault",
"kind" : "remoteSourceControl",

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
//
// 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 {
let translate_url = damus_state.settings.libretranslate_url
let api_key = damus_state.settings.libretranslate_api_key
guard noteLanguage == nil && !checkingTranslationStatus && translate_url != "" 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 LibreTranslate 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(translate_url, apiKey: api_key)
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)
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
translated_artifacts = render_blocks(blocks: blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, show_images: show_images)
}
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)
}
}

View File

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

View File

@@ -0,0 +1,129 @@
//
// 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
}
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: 1000000) 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()
}
}
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)
}
}

View File

@@ -12,10 +12,10 @@ import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://relay.snort.social",
"wss://nostr.orangepill.dev",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
]
@@ -73,6 +73,7 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@State var active_profile: String? = nil
@@ -88,7 +89,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()
@@ -111,7 +111,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
}
}
@@ -119,9 +119,10 @@ struct ContentView: View {
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
FiltersView
//.frame(maxWidth: 275)
.padding()
CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
@@ -137,16 +138,6 @@ struct ContentView: View {
}
}
var FiltersView: some View {
VStack{
Picker(NSLocalizedString("Filter State", comment: "Filter state for seeing either only posts, or posts & replies."), selection: $filter_state) {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
}
.pickerStyle(.segmented)
}
}
func MainContent(damus: DamusState) -> some View {
VStack {
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
@@ -261,16 +252,18 @@ struct ContentView: View {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, contacts: damus_state!.contacts)
}
}
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)
}
}
}
@@ -302,7 +295,7 @@ struct ContentView: View {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, references: [])
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
}
@@ -348,6 +341,9 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
}
.onReceive(handle_notify(.report)) { notif in
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
@@ -434,7 +430,13 @@ struct ContentView: View {
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
}
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate "), isPresented: $user_blocked_confirm, actions: {
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
notify(.logout, ())
}
}
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
@@ -557,7 +559,10 @@ struct ContentView: View {
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: UserSettingsStore()
)
home.damus_state = self.damus_state!

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 {

View File

@@ -18,17 +18,20 @@ struct DamusState {
let profiles: Profiles
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let lnurls: LNUrls
let settings: UserSettingsStore
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(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())
}
}

View File

@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):

View File

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

View File

@@ -112,9 +112,61 @@ 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, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
damus_state.zaps.add_zap(zap: zap)
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
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
}
guard ptag == damus_state.pubkey else {
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) {
handle_zap_event_with_zapper(ev, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
DispatchQueue.main.async {
self.handle_zap_event_with_zapper(ev, zapper: zapper)
}
}
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -234,7 +286,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 +369,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
@@ -372,7 +425,7 @@ class HomeModel: ObservableObject {
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -530,10 +583,17 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
}
return
}
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
@@ -727,3 +787,4 @@ func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
}
return !ev.should_show_event
}

View File

@@ -5,9 +5,8 @@
// Created by Oleg Abalonski on 1/11/23.
//
import Foundation
import UIKit
import Kingfisher
import SVGKit
class KFImageModel: ObservableObject {
@@ -80,8 +79,15 @@ struct CustomImageProcessor: ImageProcessor {
}
// Handle SVG image
if let svgImage = SVGKImage(data: data), let image = svgImage.uiImage {
return image.kf.scaled(to: options.scaleFactor)
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)

View File

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

View File

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

View File

@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
let block: Block
}
struct Invoice {
let description: String
let amount: Amount
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash:
return ""
}
}
}
enum Block {
@@ -189,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)

View File

@@ -97,7 +97,7 @@ class ProfileModel: ObservableObject, Equatable {
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(profiles: damus.profiles, ev: ev)
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}

View File

@@ -129,7 +129,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
}
if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
}

View File

@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
}
if ev.known_kind == .metadata {
process_metadata_event(profiles: damus_state.profiles, ev: ev)
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {

View File

@@ -6,6 +6,37 @@
//
import Foundation
import Vault
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
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
}
}
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)
}
func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
}
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
}
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
@@ -26,16 +57,84 @@ class UserSettingsStore: ObservableObject {
}
}
init() {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
self.default_wallet = default_wallet
} else {
default_wallet = .system_default_wallet
@Published var libretranslate_server: LibreTranslateServer {
didSet {
if oldValue == libretranslate_server {
return
}
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
libretranslate_api_key = ""
if libretranslate_server == .custom || libretranslate_server == .none {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
}
}
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
@Published var libretranslate_url: String {
didSet {
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
}
}
@Published var libretranslate_api_key: String {
didSet {
do {
if libretranslate_api_key == "" {
try clearLibreTranslateApiKey()
} else {
try saveLibreTranslateApiKey(libretranslate_api_key)
}
} catch {
// No-op.
}
}
}
init() {
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet(pubkey)
show_wallet_selector = should_show_wallet_selector(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
if let server = get_libretranslate_server(pubkey) {
self.libretranslate_server = server
self.libretranslate_url = get_libretranslate_url(pubkey, server: server) ?? ""
} 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 = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} catch {
libretranslate_api_key = ""
}
}
func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
func clearLibreTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "libretranslate_apikey"
}

View File

@@ -53,7 +53,7 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi:
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet Of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet Of Satoshi."), link: "walletofsatoshi:lightning:",
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee:
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",

View File

@@ -24,18 +24,22 @@ struct Profile: Codable {
}
private func str(_ str: String) -> String? {
guard let val = self.value[str] else{
return get_val(str)
}
private func get_val<T>(_ v: String) -> T? {
guard let val = self.value[v] else{
return nil
}
guard let s = val.value as? String else {
guard let s = val.value as? T else {
return nil
}
return s
}
private mutating func set_str(_ key: String, _ val: String?) {
private mutating func set_val<T>(_ key: String, _ val: T?) {
if val == nil {
self.value.removeValue(forKey: key)
return
@@ -44,6 +48,15 @@ struct Profile: Codable {
self.value[key] = AnyCodable.init(val)
}
private mutating func set_str(_ key: String, _ val: String?) {
set_val(key, val)
}
var deleted: Bool? {
get { return get_val("deleted"); }
set(s) { set_val("deleted", s) }
}
var display_name: String? {
get { return str("display_name"); }
set(s) { set_str("display_name", s) }
@@ -97,6 +110,10 @@ struct Profile: Codable {
return lnaddress_to_lnurl(addr);
}
if !addr.lowercased().hasPrefix("lnurl") {
return nil
}
return addr;
}
@@ -109,6 +126,10 @@ struct Profile: Codable {
return make_ln_url(self.lnurl)
}
init() {
self.value = [:]
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: AnyCodable].self)

View File

@@ -11,6 +11,8 @@ import secp256k1
import secp256k1_implementation
import CryptoKit
enum ValidationResult: Decodable {
case ok
case bad_id
@@ -27,7 +29,7 @@ struct KeyEvent {
let relay_url: String
}
struct ReferencedId: Identifiable, Hashable {
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String
@@ -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 {
@@ -103,11 +105,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
if let bs = _blocks {
return bs
}
let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags)
let blocks = get_blocks(content: self.get_content(privkey))
self._blocks = blocks
return blocks
}
func get_blocks(content: String) -> [Block] {
return parse_mentions(content: content, tags: self.tags)
}
lazy var inner_event: NostrEvent? = {
// don't try to deserialize an inner event if we know there won't be one
if self.known_kind == .boost {
@@ -367,6 +373,10 @@ func encode_json<T: Encodable>(_ val: T) -> String? {
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))
}
@@ -567,6 +577,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] } ?? []

View File

@@ -7,7 +7,7 @@
import Foundation
struct NostrFilter: Codable {
struct NostrFilter: Codable, Equatable {
var ids: [String]?
var kinds: [Int]?
var referenced_ids: [String]?

View File

@@ -20,4 +20,5 @@ enum NostrKind: Int {
case channel_meta = 41
case chat = 42
case list = 30000
case zap = 9735
}

View File

@@ -8,7 +8,7 @@
import Foundation
enum NostrLink {
enum NostrLink: Equatable {
case ref(ReferencedId)
case filter(NostrFilter)
}
@@ -101,6 +101,24 @@ func decode_universal_link(_ s: String) -> NostrLink? {
return nil
}
func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
guard let obj = Bech32Object.parse(s) else {
return nil
}
switch obj {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .npub(let pubkey):
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .note(let id):
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
}
}
func decode_nostr_uri(_ s: String) -> NostrLink? {
if s.starts(with: "https://damus.io/") {
return decode_universal_link(s)
@@ -122,5 +140,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
}
return tag_to_refid(parts).map { .ref($0) }
if let rid = tag_to_refid(parts) {
return .ref(rid)
}
guard parts.count == 1 else {
return nil
}
let part = parts[0]
return decode_nostr_bech32_uri(part)
}

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
}

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 {

View File

@@ -0,0 +1,22 @@
//
// AccountDeletion.swift
// damus
//
// Created by William Casarin on 2023-01-30.
//
import Foundation
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
var profile = Profile()
profile.deleted = true
profile.about = "account deleted"
profile.name = "nobody"
let content = encode_json(profile)!
let ev = NostrEvent(content: content, pubkey: keypair.pubkey, kind: 0)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
return ev
}

View File

@@ -0,0 +1,31 @@
//
// Bech32Object.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import Foundation
enum Bech32Object {
case nsec(String)
case npub(String)
case note(String)
static func parse(_ str: String) -> Bech32Object? {
guard let decoded = try? bech32_decode(str) else {
return nil
}
if decoded.hrp == "npub" {
return .npub(hex_encode(decoded.data))
} else if decoded.hrp == "nsec" {
return .nsec(hex_encode(decoded.data))
} else if decoded.hrp == "note" {
return .note(hex_encode(decoded.data))
}
return nil
}
}

101
damus/Util/CoreSVG.swift Normal file
View File

@@ -0,0 +1,101 @@
//
// CoreSVG.swift
// damus
//
// Created by Oleg Abalonski on 1/27/23.
// Ref: https://gist.github.com/ollieatkinson/eb87a82fcb5500d5561fed8b0900a9f7
import Darwin
import Foundation
import UIKit
@objc
class CGSVGDocument: NSObject { }
var CGSVGDocumentRetain: (@convention(c) (CGSVGDocument?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentRetain")
var CGSVGDocumentRelease: (@convention(c) (CGSVGDocument?) -> Void) = load("CGSVGDocumentRelease")
var CGSVGDocumentCreateFromData: (@convention(c) (CFData?, CFDictionary?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentCreateFromData")
var CGContextDrawSVGDocument: (@convention(c) (CGContext?, CGSVGDocument?) -> Void) = load("CGContextDrawSVGDocument")
var CGSVGDocumentGetCanvasSize: (@convention(c) (CGSVGDocument?) -> CGSize) = load("CGSVGDocumentGetCanvasSize")
typealias ImageWithCGSVGDocument = @convention(c) (AnyObject, Selector, CGSVGDocument) -> UIImage
var ImageWithCGSVGDocumentSEL: Selector = NSSelectorFromString("_imageWithCGSVGDocument:")
let CoreSVG = dlopen("/System/Library/PrivateFrameworks/CoreSVG.framework/CoreSVG", RTLD_NOW)
func load<T>(_ name: String) -> T {
unsafeBitCast(dlsym(CoreSVG, name), to: T.self)
}
public class SVG {
deinit { CGSVGDocumentRelease(document) }
let document: CGSVGDocument
public convenience init?(_ value: String) {
guard let data = value.data(using: .utf8) else { return nil }
self.init(data)
}
public init?(_ data: Data) {
guard let document = CGSVGDocumentCreateFromData(data as CFData, nil)?.takeUnretainedValue() else { return nil }
guard CGSVGDocumentGetCanvasSize(document) != .zero else { return nil }
self.document = document
}
public var size: CGSize {
CGSVGDocumentGetCanvasSize(document)
}
public func image() -> UIImage? {
let ImageWithCGSVGDocument = unsafeBitCast(UIImage.self.method(for: ImageWithCGSVGDocumentSEL), to: ImageWithCGSVGDocument.self)
let image = ImageWithCGSVGDocument(UIImage.self, ImageWithCGSVGDocumentSEL, document)
return image
}
public func draw(in context: CGContext) {
draw(in: context, size: size)
}
public func draw(in context: CGContext, size target: CGSize) {
var target = target
let ratio = (
x: target.width / size.width,
y: target.height / size.height
)
let rect = (
document: CGRect(origin: .zero, size: size), ()
)
let scale: (x: CGFloat, y: CGFloat)
if target.width <= 0 {
scale = (ratio.y, ratio.y)
target.width = size.width * scale.x
} else if target.height <= 0 {
scale = (ratio.x, ratio.x)
target.width = size.width * scale.y
} else {
let min = min(ratio.x, ratio.y)
scale = (min, min)
target.width = size.width * scale.x
target.height = size.height * scale.y
}
let transform = (
scale: CGAffineTransform(scaleX: scale.x, y: scale.y),
aspect: CGAffineTransform(translationX: (target.width / scale.x - rect.document.width) / 2, y: (target.height / scale.y - rect.document.height) / 2)
)
context.translateBy(x: 0, y: target.height)
context.scaleBy(x: 1, y: -1)
context.concatenate(transform.scale)
context.concatenate(transform.aspect)
CGContextDrawSVGDocument(context, document)
}
}

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

View File

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

View File

@@ -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
damus/Util/LNUrls.swift Normal file
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]
}
}

View File

@@ -24,7 +24,7 @@ func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_ad
}
}
var tags = [["d", list_name], [list_type, to_add]]
let tags = [["d", list_name], [list_type, to_add]]
let ev = NostrEvent(content: "", pubkey: pubkey, kind: 30000, tags: tags)
ev.tags = tags

View File

@@ -95,6 +95,9 @@ extension Notification.Name {
static var new_unmutes: Notification.Name {
return Notification.Name("new_unmutes")
}
static var deleted_account: Notification.Name {
return Notification.Name("deleted_account")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {

322
damus/Util/Zap.swift Normal file
View File

@@ -0,0 +1,322 @@
//
// 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) {
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
damus/Util/Zaps.swift Normal file
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
}
}

View File

@@ -21,12 +21,26 @@ 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 {
@@ -50,6 +64,7 @@ struct EventActionBar: View {
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Spacer()
ZStack {
LikeButton(liked: bar.liked) {
if bar.liked {
@@ -64,6 +79,12 @@ 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
@@ -155,10 +176,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 +190,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)
}

View File

@@ -28,8 +28,8 @@ 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 {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("Zaps", 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'.")
}
}
}

View File

@@ -55,6 +55,7 @@ struct CarouselItemView: View {
.font(.title2)
.foregroundColor(Color.white)
.padding([.leading,.trailing], 50.0)
.minimumScaleFactor(0.5)
}
}
}

View File

@@ -75,7 +75,7 @@ struct ChatView: View {
HStack {
VStack {
if is_active || just_started {
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles, contacts:damus_state.contacts)
}
Spacer()
@@ -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, contacts: damus_state.contacts)
.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)

View File

@@ -24,7 +24,7 @@ struct ChatroomView: View {
next_ev: ind == count-1 ? nil : thread.events[ind+1],
damus_state: damus
)
.event_context_menu(ev, keypair: damus.keypair)
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
.onTapGesture {
if thread.initial_event.id == ev.id {
//dismiss()

View File

@@ -5,30 +5,51 @@
// Created by William Casarin on 2022-06-09.
//
import AVFoundation
import SwiftUI
import Kingfisher
import SwiftUI
enum RemoteImagePolicy: String, CaseIterable {
case everyone
case friendsOnly
case friendsOfFriends
case restricted
}
func remoteImagePolicyText(_ fs: RemoteImagePolicy) -> String {
switch fs {
case .everyone:
return "Everyone"
case .friendsOnly:
return "Friends Only"
case .friendsOfFriends:
return "Friends of Friends"
case .restricted:
return "Block Images"
}
}
struct ConfigView: View {
let state: DamusState
@Environment(\.dismiss) var dismiss
@State var show_add_relay: Bool = false
@State var confirm_logout: Bool = false
@State var new_relay: String = ""
@State var confirm_delete_account: Bool = false
@State var show_privkey: Bool = false
@State var show_libretranslate_api_key: Bool = false
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var relays: [RelayDescriptor]
@EnvironmentObject var user_settings: UserSettingsStore
@State var delete_text: String = ""
@ObservedObject var settings: UserSettingsStore
@AppStorage("remote_image_policy") var remote_image_policy: RemoteImagePolicy = .everyone
let generator = UIImpactFeedbackGenerator(style: .light)
init(state: DamusState) {
self.state = state
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_relays = State(initialValue: state.pool.descriptors)
_settings = ObservedObject(initialValue: state.settings)
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
@@ -41,52 +62,19 @@ struct ConfigView: View {
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
}
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
if let _ = state.pool.get_relay(x) {
} else {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
var body: some View {
ZStack(alignment: .leading) {
Form {
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString)
}
} header: {
HStack {
Text("Relays", comment: "Header text for relay server list for configuration.")
Spacer()
Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
}
if recommended.count > 0 {
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
}
}
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
HStack {
Text(state.keypair.pubkey_bech32)
CopyButton(is_pk: true)
}
.clipShape(RoundedRectangle(cornerRadius: 5))
}
if let sec = state.keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
@@ -97,18 +85,18 @@ struct ConfigView: View {
Text(sec)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
CopyButton(is_pk: false)
}
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
}
}
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)
@@ -116,8 +104,41 @@ 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: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.libretranslate_server != .none {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(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: $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: $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
}
}
}
}
}
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)
}
@@ -128,16 +149,65 @@ struct ConfigView: View {
KingfisherManager.shared.cache.cleanExpiredDiskCache()
}
}
Section(NSLocalizedString("Reset", comment: "Section title for resetting the user")) {
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
confirm_logout = true
Section(NSLocalizedString("Remote Image Loading Policy", comment: "Section title for remote image loading policy")) {
Menu {
Button {
self.remote_image_policy = .everyone
} label: {
Text(remoteImagePolicyText(.everyone))
}
Button {
self.remote_image_policy = .friendsOfFriends
} label: {
Text(remoteImagePolicyText(.friendsOfFriends))
}
Button {
self.remote_image_policy = .friendsOnly
} label: {
Text(remoteImagePolicyText(.friendsOnly))
}
Button {
self.remote_image_policy = .restricted
} label: {
Text(remoteImagePolicyText(.restricted))
}
} label: {
Text("\(remoteImagePolicyText(remote_image_policy))")
.frame(maxWidth: .infinity, alignment: .leading)
}
}
if state.is_privkey_user {
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
confirm_delete_account = true
}
}
}
}
}
.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)
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
confirm_delete_account = false
}
Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) {
guard let full_kp = state.keypair.to_full() else {
return
}
guard delete_text == "DELETE" else {
return
}
let ev = created_deleted_account_profile(keypair: full_kp)
state.pool.send(.event(ev))
notify(.logout, ())
}
}
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
confirm_logout = false
@@ -148,51 +218,9 @@ struct ConfigView: View {
} message: {
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
return
}
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
relay = "wss://" + relay
}
guard let url = URL(string: relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
state.pool.send(.event(new_ev))
}
}
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.descriptors
}
}
}

View File

@@ -12,6 +12,7 @@ struct DMChatView: View {
let pubkey: String
@EnvironmentObject var dms: DirectMessageModel
@State var message: String = ""
@State var showPrivateKeyWarning: Bool = false
var Messages: some View {
ScrollViewReader { scroller in
@@ -19,7 +20,7 @@ struct DMChatView: View {
VStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
DMView(event: dms.events[ind], damus_state: damus_state)
.event_context_menu(ev, keypair: damus_state.keypair)
.event_context_menu(ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey)
}
EndBlock(height: 80)
}
@@ -42,7 +43,7 @@ struct DMChatView: View {
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, contacts:damus_state.contacts)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: true)
}
@@ -63,6 +64,8 @@ struct DMChatView: View {
)
.padding(16)
.foregroundColor(Color.primary)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
.fixedSize(horizontal: false, vertical: true)
}
@Environment(\.colorScheme) var colorScheme
@@ -91,28 +94,30 @@ struct DMChatView: View {
InputField
if !message.isEmpty {
Button(role: .none, action: send_message) {
Button(
role: .none,
action: {
showPrivateKeyWarning = contentContainsPrivateKey(message)
if !showPrivateKeyWarning {
send_message()
}
}
) {
Label("", systemImage: "arrow.right.circle")
.font(.title)
}
}
}
}
.frame(height: 50 + 20 * CGFloat(text_lines))
}
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
var text_lines: Int {
var lines = 1
for c in message {
if lines > 4 {
return lines
}
if c.isNewline {
lines += 1
}
Text(message).opacity(0).padding(.all, 8)
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
}
return lines
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
}
func send_message() {
@@ -142,15 +147,24 @@ struct DMChatView: View {
Footer
}
Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
.lineLimit(nil)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
.foregroundColor(.gray)
.lineLimit(nil)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
.foregroundColor(.gray)
}
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
.toolbar { Header }
.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()
}
})
}
}

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

View File

@@ -43,6 +43,7 @@ struct DirectMessagesView: View {
}
}
}
.padding(.horizontal)
}
}
@@ -62,16 +63,16 @@ struct DirectMessagesView: View {
}
var body: some View {
VStack {
Picker(NSLocalizedString("DM Type", comment: "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."), selection: $dm_type) {
VStack(spacing: 0) {
CustomPicker(selection: $dm_type, content: {
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
.tag(DMType.friend)
Text("Requests", comment: "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.")
.tag(DMType.rando)
}
.pickerStyle(.segmented)
})
Divider()
.frame(height: 1)
TabView(selection: $dm_type) {
MainContent(requests: false)
@@ -82,8 +83,6 @@ struct DirectMessagesView: View {
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.padding(.horizontal)
.padding(.top)
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
}
}

View File

@@ -120,7 +120,7 @@ struct EditMetadataView: View {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, contacts: damus_state.contacts)
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
@@ -196,7 +196,7 @@ struct EditMetadataView: View {
if let parts = nip05_parts {
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
} else {
Text("'\(nip05)' is an invalid nip05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
}
})

View File

@@ -71,7 +71,7 @@ struct EventDetailView: View {
}
toggle_thread_view()
}
case .event(let ev, let _):
case .event(let ev, _):
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
if thread.initial_event.id == ev.id {

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)
}
}
// blame the porn bots for this code
@@ -138,12 +98,18 @@ func should_show_images(contacts: Contacts, ev: NostrEvent, our_pubkey: String,
if ev.pubkey == our_pubkey {
return true
}
if contacts.is_in_friendosphere(ev.pubkey) {
let remote_image_policy: RemoteImagePolicy = RemoteImagePolicy(rawValue: UserDefaults.standard.string(forKey: "remote_image_policy") ?? "") ?? .friendsOfFriends
if remote_image_policy == .everyone ||
remote_image_policy == .friendsOnly && contacts.is_friend(ev.pubkey) ||
remote_image_policy == .friendsOfFriends && contacts.is_in_friendosphere(ev.pubkey) {
return true
}
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) {
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) && remote_image_policy != .restricted {
return true
}
return false
}
@@ -171,9 +137,9 @@ extension View {
}
}
func event_context_menu(_ event: NostrEvent, keypair: Keypair) -> some View {
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
return self.contextMenu {
EventMenuContext(event: event, keypair: keypair)
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
}
}
@@ -197,17 +163,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
)
}

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

View File

@@ -23,7 +23,7 @@ struct EmbeddedEventView: View {
EventBody(damus_state: damus_state, event: event, size: .small)
}
.event_context_menu(event, keypair: damus_state.keypair)
.event_context_menu(event, keypair: damus_state.keypair, target_pubkey: pubkey)
}
}

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

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct EventMenuContext: View {
let event: NostrEvent
let keypair: Keypair
let target_pubkey: String
var body: some View {
@@ -20,7 +21,7 @@ struct EventMenuContext: View {
}
Button {
UIPasteboard.general.string = keypair.pubkey_bech32
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
} label: {
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
}
@@ -44,16 +45,16 @@ struct EventMenuContext: View {
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if keypair.pubkey != event.pubkey && keypair.privkey != nil {
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
Button(role: .destructive) {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: event.pubkey, note_id: event.id))
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
notify(.report, target)
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
}
Button(role: .destructive) {
notify(.block, event.pubkey)
notify(.block, target_pubkey)
} label: {
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
}

View File

@@ -35,7 +35,7 @@ struct EventProfile: View {
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, contacts: damus_state.contacts)
}
}

View File

@@ -42,9 +42,9 @@ struct MutedEventView: View {
.foregroundColor(FillColor)
HStack {
Text("Post from a user you've blocked")
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
Spacer()
Button(shown ? "Hide" : "Show") {
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been blocked.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been blocked.")) {
shown.toggle()
}
}

View File

@@ -49,7 +49,7 @@ struct SelectedEventView: View {
.padding([.top], 4)
}
.padding([.leading], 2)
.event_context_menu(event, keypair: damus.keypair)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
}
}
}

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, contacts: damus.contacts)
}
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)
}
}

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))")
.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()
}
}
*/

View File

@@ -7,75 +7,33 @@
import SwiftUI
import LinkPresentation
import NaturalLanguage
struct NoteArtifacts {
let content: String
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: content, images: [], invoices: [], links: [])
}
}
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let txt = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + txt
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
invoices.append(invoice)
return str
case .url(let url):
// Handle Image URLs
if is_image_url(url) {
// Append Image
img_urls.append(url)
return str
} else {
link_urls.append(url)
return str + url.absoluteString
}
}
}
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
}
func is_image_url(_ url: URL) -> Bool {
let str = url.lastPathComponent.lowercased()
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
}
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct NoteContentView: View {
let privkey: String?
let damus_state: DamusState
let event: NostrEvent
let profiles: Profiles
let previews: PreviewCache
let show_images: Bool
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable? = nil
let size: EventViewKind
@State var preview: LinkViewRepresentable? = nil
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(Markdown.parse(content: artifacts.content))
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 {
@@ -88,7 +46,7 @@ struct NoteContentView: View {
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(invoices: artifacts.invoices)
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
if let preview = self.preview, show_images {
@@ -107,16 +65,16 @@ struct NoteContentView: View {
var body: some View {
MainContent()
.onAppear() {
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, show_images: show_images)
}
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(privkey)
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: profiles, privkey: privkey)
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, show_images: show_images)
}
case .text: return
case .hashtag: return
@@ -126,7 +84,7 @@ struct NoteContentView: View {
}
}
.task {
if let preview = previews.lookup(self.event.id) {
if let preview = damus_state.previews.lookup(self.event.id) {
switch preview {
case .value(let view):
self.preview = view
@@ -140,13 +98,13 @@ struct NoteContentView: View {
let meta = await getMetaData(for: artifacts.links.first!)
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
previews.store(evid: self.event.id, preview: view)
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 {
@@ -163,20 +121,112 @@ struct NoteContentView: View {
}
}
func hashtag_str(_ htag: String) -> String {
return "[#\(htag)](nostr:t:\(htag))"
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> String {
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)
return "[@\(disp)](nostr:\(encode_pubkey_uri(m.ref)))"
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
return "[@\(abbrev_pubkey(bevid))](nostr:\(encode_event_id_uri(m.ref)))"
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() }
)
}
}
@@ -185,7 +235,70 @@ 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: content, images: [], invoices: [], links: [])
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal)
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
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
}
}
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?, show_images: Bool) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey, show_images: show_images)
}
func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?, show_images: Bool) -> NoteArtifacts {
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let txt: AttributedString = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + AttributedString(stringLiteral: txt)
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
invoices.append(invoice)
return str
case .url(let url):
// Handle Image URLs
if show_images && is_image_url(url) {
// Append Image
img_urls.append(url)
return str
} else {
link_urls.append(url)
return str + url_str(url)
}
}
}
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
}
func is_image_url(_ url: URL) -> Bool {
let str = url.lastPathComponent.lowercased()
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
}

View File

@@ -39,7 +39,7 @@ struct ParticipantsView: View {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, contacts: damus_state.contacts)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)

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

View File

@@ -16,10 +16,12 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: String = ""
let replying_to: NostrEvent?
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -64,7 +66,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()
}
}
}
}
@@ -74,6 +80,7 @@ struct PostView: View {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
@@ -82,6 +89,14 @@ struct PostView: View {
.allowsHitTesting(false)
}
}
// This if-block observes @ for tagging
if let searching = get_searching_string(post) {
VStack {
Spacer()
UserSearch(damus_state: damus_state, search: searching, post: $post)
}.zIndex(1)
}
}
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -89,6 +104,34 @@ struct PostView: View {
}
}
.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()
}
})
}
}
func get_searching_string(_ post: String) -> String? {
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else {
return nil
}
guard last_word.count >= 2 else {
return nil
}
guard last_word.first! == "@" else {
return nil
}
// don't include @npub... strings
guard last_word.count != 64 else {
return nil
}
return String(last_word.dropFirst())
}

View File

@@ -0,0 +1,87 @@
//
// UserAutocompletion.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import SwiftUI
struct SearchedUser: Identifiable {
let petname: String?
let profile: Profile?
let pubkey: String
var id: String {
return pubkey
}
}
struct UserSearch: View {
let damus_state: DamusState
let search: String
@Binding var post: String
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
return []
}
return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search)
}
var body: some View {
ScrollView {
LazyVStack {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
}
}
}
}
}
}
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: String = "some @jb55"
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
}
}
func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] {
var seen_user = Set<String>()
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
guard !seen_user.contains(pubkey) else {
return
}
seen_user.insert(pubkey)
var petname: String? = nil
if tag.count >= 4 {
petname = tag[3]
}
let profile = profiles.lookup(id: pubkey)
guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
}

View File

@@ -98,19 +98,21 @@ struct ProfilePicView: View {
let size: CGFloat
let highlight: Highlight
let profiles: Profiles
let contacts: Contacts
@State var picture: String?
init (pubkey: String, size: CGFloat, highlight: Highlight, profiles: Profiles, picture: String? = nil) {
init (pubkey: String, size: CGFloat, highlight: Highlight, profiles: Profiles, contacts: Contacts, picture: String? = nil) {
self.pubkey = pubkey
self.profiles = profiles
self.contacts = contacts
self.size = size
self.highlight = highlight
self._picture = State(initialValue: picture)
}
var body: some View {
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight)
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles, contacts: contacts), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight)
.onReceive(handle_notify(.profile_updated)) { notif in
let updated = notif.object as! ProfileUpdate
@@ -125,8 +127,19 @@ struct ProfilePicView: View {
}
}
func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> URL {
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
func get_profile_url(picture: String?, pubkey: String, profiles: Profiles, contacts: Contacts) -> URL {
var pic: String
let remote_image_policy: RemoteImagePolicy = RemoteImagePolicy(rawValue: UserDefaults.standard.string(forKey: "remote_image_policy") ?? "") ?? .friendsOfFriends
if pubkey == contacts.our_pubkey ||
remote_image_policy == .everyone ||
remote_image_policy == .friendsOnly && contacts.is_friend(pubkey) ||
remote_image_policy == .friendsOfFriends && contacts.is_in_friendosphere(pubkey) {
pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
} else {
pic = robohash(pubkey)
}
if let url = URL(string: pic) {
return url
}
@@ -150,7 +163,8 @@ struct ProfilePicView_Previews: PreviewProvider {
pubkey: pubkey,
size: 100,
highlight: .none,
profiles: make_preview_profiles(pubkey))
profiles: make_preview_profiles(pubkey),
contacts: Contacts(our_pubkey: pubkey))
}
}

View File

@@ -13,7 +13,7 @@ struct ProfilePictureSelector: View {
var body: some View {
let highlight: Highlight = .custom(Color.white, 2.0)
ZStack {
ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles())
ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles(), contacts: Contacts(our_pubkey: pubkey))
}
}
}

View File

@@ -91,6 +91,8 @@ struct EditButton: View {
RoundedRectangle(cornerRadius: 24)
.stroke(borderColor(), lineWidth: 1)
}
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
@@ -115,7 +117,6 @@ struct ProfileView: View {
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var action_sheet_presented: Bool = false
@StateObject var user_settings = UserSettingsStore()
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@@ -141,10 +142,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")
@@ -160,8 +161,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)
}
}
@@ -174,6 +174,24 @@ struct ProfileView: View {
Image(systemName: "ellipsis.circle")
.profile_button_style(scheme: colorScheme)
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var ShareButton: some View {
@@ -237,12 +255,12 @@ struct ProfileView: View {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, contacts: damus_state.contacts)
.onTapGesture {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles, contacts: damus_state.contacts)}
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
@@ -279,7 +297,7 @@ struct ProfileView: View {
.padding(.top,-(pfp_size/2.0))
Text(ProfileView.markdown.process(data?.about ?? ""))
.font(.subheadline)
.font(.subheadline).textSelection(.enabled)
if let url = data?.website_url {
WebsiteLink(url: url)
@@ -374,23 +392,6 @@ struct ProfileView: View {
}
}
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
.ignoresSafeArea()
}
}
@@ -407,7 +408,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)

View File

@@ -11,6 +11,7 @@ struct ProfileZoomView: View {
@Environment(\.presentationMode) var presentationMode
let pubkey: String
let profiles: Profiles
let contacts: Contacts
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero
@@ -68,7 +69,7 @@ struct ProfileZoomView: View {
Spacer()
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles)
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles, contacts: contacts)
.padding(100)
.scaledToFit()
.scaleEffect(self.scale * scaleState)
@@ -92,6 +93,8 @@ struct ProfileZoomView_Previews: PreviewProvider {
static var previews: some View {
ProfileZoomView(
pubkey: pubkey,
profiles: make_preview_profiles(pubkey))
profiles: make_preview_profiles(pubkey),
contacts: Contacts(our_pubkey: pubkey)
)
}
}

View File

@@ -0,0 +1,124 @@
//
// QRCodeView.swift
// damus
//
// Created by eric on 1/27/23.
//
import SwiftUI
import CoreImage.CIFilterBuiltins
struct QRCodeView: View {
let damus_state: DamusState
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
var maybe_key: String? {
guard let key = bech32_pubkey(damus_state.pubkey) else {
return nil
}
return key
}
var body: some View {
ZStack(alignment: .center) {
ZStack(alignment: .topLeading) {
DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.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, contacts: damus_state.contacts)
.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()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color("DamusWhite"), lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on nostr")
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code")
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 18, weight: .ultraLight))
Spacer()
}
}
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
}
func generateQRCode(pubkey: String) -> UIImage {
let data = pubkey.data(using: String.Encoding.ascii)
let qrFilter = CIFilter(name: "CIQRCodeGenerator")
qrFilter?.setValue(data, forKey: "inputMessage")
let qrImage = qrFilter?.outputImage
let colorInvertFilter = CIFilter(name: "CIColorInvert")
colorInvertFilter?.setValue(qrImage, forKey: "inputImage")
let outputInvertedImage = colorInvertFilter?.outputImage
let maskToAlphaFilter = CIFilter(name: "CIMaskToAlpha")
maskToAlphaFilter?.setValue(outputInvertedImage, forKey: "inputImage")
let outputCIImage = maskToAlphaFilter?.outputImage
let context = CIContext()
let cgImage = context.createCGImage(outputCIImage!, from: outputCIImage!.extent)!
return UIImage(cgImage: cgImage)
}
}
struct QRCodeView_Previews: PreviewProvider {
static var previews: some View {
QRCodeView(damus_state: test_damus_state())
}
}

View File

@@ -0,0 +1,109 @@
//
// RelayConfigView.swift
// damus
//
// Created by William Casarin on 2023-01-30.
//
import SwiftUI
struct RelayConfigView: View {
let state: DamusState
@State var new_relay: String = ""
@State var show_add_relay: Bool = false
@State var relays: [RelayDescriptor]
init(state: DamusState) {
self.state = state
_relays = State(initialValue: state.pool.descriptors)
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
if let _ = state.pool.get_relay(x) {
} else {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
var body: some View {
MainContent
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.descriptors
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
return
}
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
relay = "wss://" + relay
}
guard let url = URL(string: relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
state.pool.send(.event(new_ev))
}
}
}
var MainContent: some View {
Form {
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString)
}
} header: {
HStack {
Text("Relays", comment: "Header text for relay server list for configuration.")
Spacer()
Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
}
if recommended.count > 0 {
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
}
}
}
}
}
struct RelayConfigView_Previews: PreviewProvider {
static var previews: some View {
RelayConfigView(state: test_damus_state())
}
}

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

View File

@@ -47,7 +47,7 @@ struct ReplyView: View {
ScrollView {
EventView(damus: damus, event: replying_to, has_action_bar: false)
}
PostView(replying_to: replying_to, references: references)
PostView(replying_to: replying_to, references: references, damus_state: damus)
}
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)

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

View File

@@ -10,9 +10,9 @@ import SwiftUI
struct SideMenuView: View {
let damus_state: DamusState
@Binding var isSidebarVisible: Bool
@State var confirm_logout: Bool = false
@StateObject var user_settings = UserSettingsStore()
@State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme
@@ -55,7 +55,7 @@ struct SideMenuView: View {
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)
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, contacts: damus_state.contacts, picture: picture)
} else {
Image(systemName: "person.fill")
}
@@ -113,7 +113,13 @@ struct SideMenuView: View {
.foregroundColor(textColor())
}
NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) {
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)) {
Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
.font(.title2)
.foregroundColor(textColor())
@@ -124,18 +130,33 @@ struct SideMenuView: View {
Spacer()
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
HStack(alignment: .center) {
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
Spacer()
Button(action: {
showQRCode.toggle()
}, label: {
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)
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
}
}
.padding(.top, 60)
.padding(.bottom, 40)

View File

@@ -0,0 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "دامُس";
/* Bundle name */
"CFBundleName" = "دامُس";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "السماح لدامُس بالوصول إلى الصور يتيح لك حفظ الصور";

View File

@@ -0,0 +1,631 @@
/* 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 profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* 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 profiles a user is following. */
"%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 >";
/* No comment provided by engineer. */
"⚡️ %@" = "⚡️ %@";
/* 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" = "كم";
/* Example URL to LibreTranslate 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" = "حذف";
/* 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 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!" = "هيا بنا!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "ترجمة LibreTranslate";
/* 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";
/* 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 server. */
"None" = "لا اختيار";
/* 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" = "رفض";
/* 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 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 hide 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)";
/* 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" = "نعم، استبدل";
/* 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";

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>

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 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)) Follower";
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) folgt";
@@ -20,10 +20,13 @@
Abbreviated version of a nostr public key. */
"%@" = "%@";
/* Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
/* 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'. */
"%@ %@" = "%@ %@";
/* 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." = "%@. Ein Konto zu erstellen benötigt keine Telefonnummer, Emailadresse oder Namen. Fang jetzt gleich ganz reibungslos an.";
@@ -33,7 +36,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
/* 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 reposts.
/* Number of tip payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
@@ -52,9 +55,15 @@ Number of profiles a user is following. */
/* Placeholder text for About Me description. */
"Absolute Boss" = "Absoluter Macher";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Zustimmen";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto ID";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Handlungen";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Hinzufügen";
@@ -68,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?";
@@ -86,6 +98,23 @@ Number of profiles a user is following. */
/* 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" = "Blockieren";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "%@ blockieren?";
/* Title of alert for blocking a user. */
"Block User" = "Benutzer blockieren";
/* Sidebar menu label for Profile view. */
"Blocked" = "Blockiert";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Blockierte Benutzer";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
@@ -95,9 +124,12 @@ Number of profiles a user is following. */
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Senden";
/* Button to cancel out of posting a note.
/* 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" = "Abbrechen";
@@ -141,11 +173,17 @@ Number of profiles a user is following. */
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Notiz JSON kopieren";
/* Button to copy report ID. */
"Copy Report ID" = "Meldungs-ID kopieren";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Text kopieren";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Benutzer ID kopieren";
"Copy User Pubkey" = "Öffentlichen Schlüssel des Benutzers kopieren";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Der zu blockierende Benutzer konnte nicht gefunden werden...";
/* Button to create account. */
"Create" = "Erstellen";
@@ -153,16 +191,25 @@ Number of profiles a user is following. */
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Neue Stummschaltungsliste";
/* 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 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. */
@@ -171,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";
@@ -199,18 +243,18 @@ Number of profiles a user is following. */
/* Error message indicating why saving keys failed. */
"Error: %@" = "Fehler: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Label describing followers of a user. */
"Followers" = "Follower";
"Followers" = "Gefolgte:r";
/* 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" = "Folgt";
"Following" = "Gefolgt";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Folge…";
@@ -219,7 +263,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Follows" = "Folgt";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Weltweit";
"Global" = "Allgemein";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Gehe zum Beitrag %@";
@@ -227,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";
@@ -236,9 +286,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Illegaler Inhalt";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* Button for user to report that the account or content has spam. */
"It's spam" = "Es ist Spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
@@ -248,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";
@@ -266,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. */
@@ -278,9 +337,15 @@ 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";
/* 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 wird eine frühere Blockier-Liste überschreiben.";
/* 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!";
@@ -293,6 +358,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Nacktheit oder anstößige Inhalte";
/* Label indicating that a form input is optional. */
"optional" = "optional";
@@ -308,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";
@@ -344,12 +415,19 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Empfohlene Relays";
/* 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. */
"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. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "Relays wurden benachrichtigt und Anwendungen können diese Information nutzen, um Inhalte zu filtern. Vielen Dank!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Alle entfernen";
@@ -362,6 +440,16 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Antwort an:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Melden";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "Meldungs-ID";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Meldung versandt!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Selbst teilen";
@@ -369,12 +457,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Selbst geteilt";
/* Navigation bar title for Reposts view. */
"Reposts" = "Geteilte Beiträge";
/* 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";
@@ -411,16 +499,24 @@ 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";
/* Button to share an image. */
/* 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 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";
@@ -430,6 +526,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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!" = "Danke!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Sie gibt sich für jemand anderen aus";
/* 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." = "Dies ist ein öffentlicher Schlüssel, mit dem Sie keine Beiträge verfassen oder in irgendeiner Weise interagieren können. Er wird verwendet, um Konten aus deren Perspektive zu betrachten.";
@@ -446,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...";
@@ -464,6 +572,15 @@ 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";
/* 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. */
"User has been blocked" = "Der Benutzer wurde blockiert";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Benutzername";
@@ -471,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";
@@ -486,15 +603,24 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to welcome user. */
"Welcome, %@!" = "Willkommen, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Was möchtest du melden?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://ein.relay.at";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Ja, überschreiben";
/* 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";
/* Label for Your Name section of user profile form. */
"Your Name" = "Dein 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" = "Die Meldung wird an Relays versendet, mit denen du verbunden bist";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";

View File

@@ -27,9 +27,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Follower</string>
<string>Gefolgte:r</string>
<key>other</key>
<string>Follower</string>
<string>Gefolgte</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>

View File

@@ -5,5 +5,5 @@
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Damus Zugriff auf deine Fotos zu gewähren erlaubt dir Bilder zu sichern.";
"NSPhotoLibraryAddUsageDescription" = "Zum Speichern von Bildern braucht Damus Zugriff auf deine Fotos.";

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 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)) Follower";
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) folgt";
@@ -16,24 +16,26 @@
/* 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 reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
/* 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 profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* 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." = "Für das Erstellen eines Accounts ist keine Telefonnumer, E-Mail-Adresse und kein Name notwendig. Lege direkt los!";
"%@. 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üsselte private Nachrichten. Halte Big Tech aus deinen Direktnachrichten 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 reposts.
/* Number of zap payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
@@ -43,6 +45,9 @@ Number of profiles a user is following. */
/* Placeholder for event mention. */
"< e >" = "< e >";
/* No comment provided by engineer. */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Über";
@@ -52,9 +57,15 @@ Number of profiles a user is following. */
/* Placeholder text for About Me description. */
"Absolute Boss" = "Absoluter Macher";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Zustimmen";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto-ID";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Aktionen";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Hinzufügen";
@@ -66,16 +77,19 @@ Number of profiles a user is following. */
"Add Relay" = "Relay hinzufügen";
/* Any amount of sats */
"Any" = "beliebig";
"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?";
"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" = "Banner Bild";
"Banner Image" = "Bannerbild";
/* 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." = "Bevor wir anfangen, musst du deine Kontodaten sichern, sonst kannst du dich in Zukunft nicht mehr anmelden, wenn du Damus jemals deinstallierst.";
"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." = "Bevor wir anfangen, musst du deine Kontodaten sichern, sonst kannst du dich in Zukunft nicht mehr anmelden, wenn du Damus einmal deinstallierst.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
@@ -86,6 +100,23 @@ Number of profiles a user is following. */
/* 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" = "Blockieren";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "%@ blockieren?";
/* Title of alert for blocking a user. */
"Block User" = "Benutzer blockieren";
/* Sidebar menu label for Profile view. */
"Blocked" = "Blockiert";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Blockierte Benutzer";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
@@ -95,9 +126,12 @@ Number of profiles a user is following. */
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Senden";
/* Button to cancel out of posting a note.
/* 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" = "Abbrechen";
@@ -105,7 +139,7 @@ Number of profiles a user is following. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Unterhaltung";
"Chat" = "Unterhaltungen";
/* Button for clearing cached data. */
"Clear" = "Löschen";
@@ -130,7 +164,7 @@ Number of profiles a user is following. */
"Copy Image URL" = "Bild-URL kopieren";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Rechnung kopieren";
"Copy invoice" = "Zahlungsdaten kopieren";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "LNURL kopieren";
@@ -141,11 +175,17 @@ Number of profiles a user is following. */
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Notiz-JSON kopieren";
/* Button to copy report ID. */
"Copy Report ID" = "Meldungs-ID kopieren";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Text kopieren";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Benutzer-ID kopieren";
"Copy User Pubkey" = "Öffentlichen Schlüssel des Benutzers kopieren";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Der zu blockierende Benutzer konnte nicht gefunden werden...";
/* Button to create account. */
"Create" = "Erstellen";
@@ -153,16 +193,25 @@ Number of profiles a user is following. */
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Neue Stummschaltungsliste";
/* 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";
/* 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 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. */
@@ -171,9 +220,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 Art";
/* 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";
@@ -199,8 +245,8 @@ Number of profiles a user is following. */
/* Error message indicating why saving keys failed. */
"Error: %@" = "Fehler: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Button to follow a user. */
"Follow" = "Folgen";
@@ -210,7 +256,7 @@ Number of profiles a user is following. */
/* 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" = "Folgt";
"Following" = "Gefolgt";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Folge…";
@@ -219,7 +265,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Follows" = "Folgt";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Weltweit";
"Global" = "Allgemein";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Gehe zum Beitrag %@";
@@ -227,18 +273,30 @@ 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";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://beispiel.de/bild.jpg";
"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" = "Illegaler Inhalt";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* Button for user to report that the account or content has spam. */
"It's spam" = "Das ist Spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
@@ -246,7 +304,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!";
/* 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";
@@ -266,11 +327,11 @@ 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. */
"Logout" = "Abmelden";
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. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Stelle sicher dass dein nsec Kontoschlüssel gesichert ist bevor du dich abmeldest oder du wirst den Zugang zu diesem Konto verlieren";
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Sorge dafür dass dein nsec Kontoschlüssel vor dem Abmelden gesichert ist, oder du wirst den Zugang zu diesem Konto verlieren";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
@@ -278,11 +339,17 @@ 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";
/* 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";
/* Dropdown option for selecting no translation server. */
"None" = "Kein";
/* 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!";
"Nothing to see here. Check back later!" = "Hier gibt es nichts zu sehen. Schau später wieder vorbei!";
/* Navigation title for notifications. */
"Notifications" = "Benachrichtigungen";
@@ -293,6 +360,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Nacktheit oder anstößige Inhalte";
/* Label indicating that a form input is optional. */
"optional" = "optional";
@@ -308,6 +378,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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";
@@ -344,12 +417,19 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Empfohlene Relays";
/* 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. */
"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. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "Relays wurden benachrichtigt und Anwendungen können diese Information nutzen, um Inhalte zu filtern. Vielen Dank!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Alle entfernen";
@@ -362,6 +442,16 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Du antwortest:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Melden";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "Meldungs-ID";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Meldung versandt!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Teilen";
@@ -369,14 +459,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Geteilt";
/* Navigation bar title for Reposts view. */
"Reposts" = "Geteilte Beiträge";
/* 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" = "Wiederholung";
"Retry" = "Erneut versuchen";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
@@ -391,7 +481,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Save" = "Speichern";
/* Context menu option to save an image. */
"Save Image" = "Bild speichern";
"Save Image" = "Bild sichern";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Hashtag suchen: #%@";
@@ -403,24 +493,32 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Secret Account Login Key" = "Geheimer Konto-Anmeldeschlüssel";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Wähle eine Lightning-Wallet";
"Select a Lightning wallet" = "Wähle ein Lightning Wallet";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Wähle ein voreingestelltes Wallet";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sende eine Nachricht um die Unterhaltung zu beginnen...";
"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";
/* Button to share an image. */
/* 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 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 anzeigen";
@@ -430,11 +528,17 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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!" = "Danke!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Die geben sich für jemand anderen aus";
/* 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." = "Dies ist ein öffentlicher Schlüssel, Du wirst keine Beiträge teilen oder oder auf irgendeine Weise interagieren können. Dies wird genutzt um Kontos aus deren Perspektive zu sehen.";
"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 veralteter nostr-Schlüssel. Wir sind und unsicher 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.";
@@ -446,6 +550,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" = "Notiz übersetzen";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "Übersetzt aus (lang)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Schreibe deinen Beitrag hier...";
@@ -456,7 +566,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Unfollow" = "Entfolgen";
/* 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" = "Entfolgen...";
"Unfollowing" = "Entfolge...";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Entfolgen...";
@@ -464,6 +574,15 @@ 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";
/* 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. */
"User has been blocked" = "Der Benutzer wurde blockiert";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Benutzername";
@@ -471,8 +590,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";
@@ -486,8 +605,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to welcome user. */
"Welcome, %@!" = "Willkommen, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Was möchtest du melden?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://irgendein.relay.de";
"wss://some.relay.com" = "wss://some.relay.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Ja, überschreiben";
/* 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";
@@ -495,6 +620,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Label for Your Name section of user profile form. */
"Your Name" = "Dein 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" = "Die Meldung wird an Relays versendet, mit denen du verbunden bist";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";

View File

@@ -29,7 +29,7 @@
<key>one</key>
<string>Follower</string>
<key>other</key>
<string>Follower</string>
<string>Gefolgte</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
@@ -77,9 +77,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; %d andere</string>
<string> &amp; %d andere</string>
<key>other</key>
<string>&amp; %d andere</string>
<string> &amp; %d andere</string>
<key>zero</key>
<string></string>
</dict>
@@ -95,9 +95,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; %d andere</string>
<string> &amp; %d andere</string>
<key>other</key>
<string>&amp; %d andere</string>
<string> &amp; %d andere</string>
<key>zero</key>
<string></string>
</dict>
@@ -113,9 +113,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Mal geteilt</string>
<string>geteilter Beitrag</string>
<key>other</key>
<string>Mal geteilt</string>
<string>geteilte Beiträge</string>
</dict>
</dict>
<key>sats_count</key>
@@ -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>

View File

@@ -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>Tip</string>
<string>Zap</string>
<key>other</key>
<string>Tips</string>
<string>Zaps</string>
</dict>
</dict>
</dict>

View File

@@ -5,5 +5,5 @@
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Si le concedes acceso a Damus a tu fototeca, podrás guardar fotos.";
"NSPhotoLibraryAddUsageDescription" = "Si le concedes acceso a Damus a tus fotos, podrás guardar imágenes.";

View File

@@ -2,10 +2,10 @@
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "'%@' en '%@' se usarán con fines de verificación";
"'%@' at '%@' will be used for verification" = "'%@' en '%@' se usará con fines de verificación";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' es un identificador nip05 no válido. Debería de tener la apariencia de un correo electrónico.";
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "'%@' es un identificador NIP-05 no válido. Debería de tener la apariencia de un correo electrónico.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Seguidores de (Profile.displayName(profile: profile, pubkey: whos))";
@@ -16,14 +16,16 @@
/* 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 reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
/* 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 profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "Se bloqueó a %@";
/* 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." = "%@. No se requiere un número de teléfono, correo electrónico ni nombre para crear una cuenta. Comienza de inmediato sin fricciones.";
@@ -33,7 +35,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
/* 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." = "%@. Deja propinas en las publicaciones de tus amigos y acumula sats con Bitcoin⚡, la moneda nativa de internet.";
/* Number of reposts.
/* Number of zap payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
@@ -43,6 +45,9 @@ Number of profiles a user is following. */
/* Placeholder for event mention. */
"< e >" = "< e >";
/* No comment provided by engineer. */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Información";
@@ -52,19 +57,31 @@ Number of profiles a user is following. */
/* Placeholder text for About Me description. */
"Absolute Boss" = "Jefe supremo";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Aceptar";
/* Label to indicate the public ID of the account. */
"Account ID" = "Identificador de cuenta";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Acciones";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Agregar";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Agregar todo";
/* Label for section for adding a relay server. */
"Add Relay" = "Agregar relé";
/* Any amount of sats */
"Any" = "Cualquiera";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "Clave de API (opcional)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "¿Seguro quieres volver a publicar esto?";
@@ -83,6 +100,23 @@ Number of profiles a user is following. */
/* 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" = "Bloquear";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "¿Bloquear a %@?";
/* Title of alert for blocking a user. */
"Block User" = "Bloquear usuario";
/* Sidebar menu label for Profile view. */
"Blocked" = "Bloqueado";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Usuarios bloqueados";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
@@ -92,9 +126,12 @@ Number of profiles a user is following. */
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Transmitir";
/* Button to cancel out of posting a note.
/* 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" = "Cancelar";
@@ -138,11 +175,17 @@ Number of profiles a user is following. */
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copiar JSON de nota";
/* Button to copy report ID. */
"Copy Report ID" = "Copiar identificador de reporte";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copiar texto";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copiar identificador de usuario";
"Copy User Pubkey" = "Copiar clave pública de usuario";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "No se pudo encontrar al usuario para bloquearlo...";
/* Button to create account. */
"Create" = "Crear";
@@ -150,16 +193,25 @@ Number of profiles a user is following. */
/* Button to create an account. */
"Create Account" = "Crear cuenta";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Crear nueva lista de silenciados";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Creador(es) de Bitcoin. Toda una leyenda.";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Personalizado";
/* 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" = "Billetera predeterminada";
/* Button to delete a relay server that the user connects to. */
/* 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" = "Borrar";
/* Button to dismiss a text field alert. */
@@ -168,8 +220,9 @@ Number of profiles a user is following. */
/* Label to prompt display name entry. */
"Display Name" = "Mostrar nombre";
/* Navigation title for DM view, which is the English abbreviation for Direct Message. */
"DM" = "MD";
/* 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" = "Mensajes directos";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Listo";
@@ -180,20 +233,20 @@ Number of profiles a user is following. */
/* Button to edit user's profile. */
"Edit" = "Editar";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Editar participantes";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Cifrada";
/* Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message. */
"Encrypted DMs" = "MD cifrados";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Ingresa la clave de tu cuenta para iniciar sesión:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Error: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Estado del filtro";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "CLUF";
/* Button to follow a user. */
"Follow" = "Seguir";
@@ -220,6 +273,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Navigation link to go to profile. */
"Goto profile %@" = "Ir al perfil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Ocultar";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "Ocultar clave de API";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Inicio";
@@ -229,9 +288,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Contenido ilegal";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Clave inválida";
/* Button for user to report that the account or content has spam. */
"It's spam" = "Es spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
@@ -241,6 +306,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!" = "¡Vamos!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "Traducciones de LibreTranslate";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Dirección de Lightning o LNURL";
@@ -259,7 +327,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" = "Cerrar sesión";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
@@ -271,9 +339,15 @@ 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" = "Verificación NIP-05";
/* 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 se encontró una lista de bloqueo. ¿Crear una nueva? Esto sobrescribirá las listas de bloqueo anteriores.";
/* No search results. */
"none" = "ninguno";
/* Dropdown option for selecting no translation server. */
"None" = "Ninguno";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Nada para ver aquí. ¡Vuelve a consultar luego!";
@@ -286,6 +360,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Desnudos o contenido explícito";
/* Label indicating that a form input is optional. */
"optional" = "opcional";
@@ -301,6 +378,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to post a note. */
"Post" = "Publicar";
/* 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" = "Publicación de un usuario que bloqueaste";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Publicaciones";
@@ -310,11 +390,8 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Privada";
/* Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account. */
"Private Key" = "Clave privada";
/* Title of the secure field that holds the user's private key. */
"PrivateKey" = "ClavePrivada";
"Private Key" = "Clave privada";
/* Sidebar menu label for Profile view. */
"Profile" = "Perfil";
@@ -340,12 +417,22 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relés recomendados";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Rechazar";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relé";
/* Sidebar menu label for Relay servers view */
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "Relés";
/* 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!" = "Se notificó a los relés, por lo que los clientes podrán usar esta información para filtrar contenido. ¡Gracias!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Eliminar todo";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Respuesta a sí mismo";
@@ -355,6 +442,16 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Respondiendo a:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Reportar";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "Identificador de reporte:";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "¡Reporte enviado!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Republicar";
@@ -362,8 +459,11 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Republicada";
/* Section title for resetting the user */
"Reset" = "Reiniciar";
/* Navigation bar title for Reposts view. */
"Reposts" = "Republicaciones";
/* 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" = "Solicitudes";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Reintentar";
@@ -387,7 +487,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Search hashtag: #%@" = "Buscar hashtag: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Búsqueda...";
"Search..." = "Buscar...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Clave de inicio de sesión de cuenta secreta";
@@ -401,16 +501,24 @@ 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..." = "Envía un mensaje para empezar la conversación...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Servidor";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Configuración";
/* Button to share an image. */
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "Compartir";
/* 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" = "Mostrar";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "Mostrar clave de API";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Mostrar selector de billetera";
@@ -420,6 +528,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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!" = "¡Gracias!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Está suplantando a alguien";
/* 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." = "Esta es una clave pública, por lo que no podrás hacer publicaciones ni interactuar de ningún modo. Se usa para ver cuentas desde su perspectiva.";
@@ -430,14 +544,20 @@ Part of a larger sentence to describe how many profiles a user is following. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Este es tu identificador de cuenta, que puedes compartir con tus amigos para que te sigan. Haz clic para copiarlo.";
/* 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!" = "Esta es tu clave de cuenta secreta, que necesitas para acceder a tu cuenta. ¡No la compartas con nadie! Guárdala en un administrador de contraseñas y protégela!";
"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!" = "Esta es tu clave de cuenta secreta, que necesitas para acceder a tu cuenta. ¡No la compartas con nadie! Guárdala en un administrador de contraseñas y protégela.";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Hilo";
/* Button to translate note from different language. */
"Translate Note" = "Traducir nota";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "Traducida del (lang)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Ingresa tu publicación aquí...";
"Type your post here..." = "Escribe tu publicación aquí...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
@@ -454,6 +574,15 @@ 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" = "Deja de seguir";
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Usuario bloqueado";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "Se bloqueó al usuario";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nombre de usuario";
@@ -461,8 +590,8 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Sidebar menu label for Wallet view. */
"Wallet" = "Billetera";
/* 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" = "Selección de billetera";
@@ -476,8 +605,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to welcome user. */
"Welcome, %@!" = "¡Te damos la bienvenida, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "¿Qué quieres reportar?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://algún.relé.com";
"wss://some.relay.com" = "wss://algun.rele.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Sí, sobrescribir";
/* 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" = "tú";
@@ -485,6 +620,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Label for Your Name section of user profile form. */
"Your Name" = "Tu nombre";
/* 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" = "El reporte se enviará a los relés con los que tengas conexión";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";

View File

@@ -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>Propina</string>
<string>Zap</string>
<key>other</key>
<string>Propinas</string>
<string>Zaps</string>
</dict>
</dict>
</dict>

View File

@@ -5,5 +5,5 @@
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Donner accès à Damus à vos photos vous permet d'enregistrer des images";
"NSPhotoLibraryAddUsageDescription" = "Accorder à Damus l'accès à vos photos vous permet d'enregistrer des images.";

View File

@@ -5,7 +5,7 @@
"'%@' at '%@' will be used for verification" = "'%@' à '@' sera utilisé pour la vérification";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'@' est un identifiant nip05 invalide. Cela devrait ressembler à une adresse e-mail.";
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "'%@' n'est pas un identifiant NIP-05 valide. Il doit avoir le format d'une adresse courriel.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Abonnés de (Profile.displayName(profile: profile, pubkey: whos))";
@@ -16,24 +16,26 @@
/* 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 reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
/* 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 profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "%@ est blocké";
/* 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." = "%@. La création d'un compte ne nécessite pas de numéro de téléphone, d'e-mail ou de nom. Commencez tout de suite sans aucune 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" = "%@. Messagerie privée cryptée de bout en bout. Gardez Big Tech hors de vos DMs";
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. Messagerie privée cryptée de bout en bout. Gardez les Big Tech hors de vos 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." = "%@. Donnez un pourboire aux publications de vos amis et empilez les sats avec Bitcoin⚡, la monnaie native d'Internet.";
"%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet." = "%@. Donnez un pourboire aux publications de vos amis et accumulez les sats avec Bitcoin⚡, la monnaie native d'Internet.";
/* Number of reposts.
/* Number of zap payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
@@ -43,17 +45,26 @@ Number of profiles a user is following. */
/* Placeholder for event mention. */
"< e >" = "< e >";
/* No comment provided by engineer. */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "À propos de";
/* Label for About Me section of user profile form. */
"About Me" = "À Propos de Moi";
"About Me" = "À propos de moi";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Patron Absolu";
"Absolute Boss" = "Patron absolu";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Accepter";
/* Label to indicate the public ID of the account. */
"Account ID" = "Identifiant de Compte";
"Account ID" = "Identifiant de compte";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Actions";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
@@ -63,16 +74,19 @@ Number of profiles a user is following. */
"Add all" = "Tout ajouter";
/* Label for section for adding a relay server. */
"Add Relay" = "Ajouter un Relais";
"Add Relay" = "Ajouter un relais";
/* Any amount of sats */
"Any" = "N'importe Lequel";
"Any" = "Montant au choix";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "Clé d'API (optionnelle)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Êtes-vous sûr de vouloir republier ceci ?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Image Bannière";
"Banner Image" = "Image de la bannière";
/* 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." = "Avant de commencer, vous devrez enregistrer les informations de votre compte, sinon vous ne pourrez plus vous connecter à l'avenir si vous désinstallez Damus.";
@@ -86,6 +100,23 @@ Number of profiles a user is following. */
/* 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" = "Bloquer";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "Bloquer %@?";
/* Title of alert for blocking a user. */
"Block User" = "Bloquer un utilisateur";
/* Sidebar menu label for Profile view. */
"Blocked" = "Bloqué";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Utilisateurs bloqués";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
@@ -95,9 +126,12 @@ Number of profiles a user is following. */
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Diffuser";
/* Button to cancel out of posting a note.
/* 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" = "Annuler";
@@ -111,7 +145,7 @@ Number of profiles a user is following. */
"Clear" = "Vider";
/* Section title for clearing cached data. */
"Clear Cache" = "Vider le Cache";
"Clear Cache" = "Vider le cache";
/* Label indicating that a user's key was copied. */
"Copied" = "Copié";
@@ -120,59 +154,71 @@ Number of profiles a user is following. */
"Copy" = "Copier";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copier l'Identifiant de Compte";
"Copy Account ID" = "Copier l'identifiant de compte";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copier l'Image";
"Copy Image" = "Copier l'image";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copier l'URL de l'Image";
"Copy Image URL" = "Copier l'URL de l'image";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copier la Facture";
"Copy invoice" = "Copier la facture";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copier le LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Copier l'Identifiant de la Note";
"Copy Note ID" = "Copier l'identifiant de la note";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copier le JSON de la Note";
"Copy Note JSON" = "Copier le JSON de la note";
/* Button to copy report ID. */
"Copy Report ID" = "Copier l'ID du signalement";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copier le Texte";
"Copy Text" = "Copier le texte";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copier l'Identifiant de l'Utilisateur";
"Copy User Pubkey" = "Copier la clé publique";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Aucun utilisateur à bloquer";
/* Button to create account. */
"Create" = "Créer";
/* Button to create an account. */
"Create Account" = "Créer un Compte";
"Create Account" = "Créer un compte";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Créer une nouvelle liste muette";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Créateur(s) de Bitcoin. Légende absolue.";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Serveur personnalisé";
/* 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" = "Portefeuille par défaut";
/* Button to delete a relay server that the user connects to. */
/* 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" = "Effacer";
/* Button to dismiss a text field alert. */
"Dismiss" = "Rejeter";
/* Label to prompt display name entry. */
"Display Name" = "Afficher Nom";
/* 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" = "Type de message privé";
"Display Name" = "Afficher le nom";
/* 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. */
@@ -182,7 +228,7 @@ Number of profiles a user is following. */
"Done" = "Fini";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Gagnes de l'argent";
"Earn Money" = "Gagne de l'argent";
/* Button to edit user's profile. */
"Edit" = "Modifier";
@@ -199,8 +245,8 @@ Number of profiles a user is following. */
/* Error message indicating why saving keys failed. */
"Error: %@" = "Erreur: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "État du filtre";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "CLUF";
/* Button to follow a user. */
"Follow" = "S'abonner";
@@ -216,7 +262,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Following..." = "Abonnements...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Suit";
"Follows" = "S'abonner";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Global";
@@ -227,6 +273,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Navigation link to go to profile. */
"Goto profile %@" = "Aller au profil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Cacher";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "Cacher la clé d'API";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Accueil";
@@ -236,9 +288,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Contenu illégal";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Clé non valide";
/* Button for user to report that the account or content has spam. */
"It's spam" = "C'est du pourriel";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
@@ -248,6 +306,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!" = "Allons-y!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "Traductions LibreTranslate";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Adresse Lightning ou LNURL";
@@ -266,7 +327,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" = "Se déconnecter";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
@@ -278,9 +339,15 @@ 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" = "Vérification NIP-05";
/* 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." = "Aucune liste de blocage trouvée, voulez-vous en créer une nouvelle? Cela va écraser votre liste de blocage existante.";
/* No search results. */
"none" = "aucun";
/* Dropdown option for selecting no translation server. */
"None" = "Aucun";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Rien à voir ici. Revenez plus tard!";
@@ -293,6 +360,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Nudité ou contenu explicite";
/* Label indicating that a form input is optional. */
"optional" = "optionnel";
@@ -308,6 +378,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to post a note. */
"Post" = "Publication";
/* 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" = "Publication d'un utilisateur que vous avez bloqué";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Publications";
@@ -330,26 +403,33 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Public Account ID" = "Identifiant publique de compte";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Clé Publique";
"Public key" = "Clé publique";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Clé Publique";
"Public Key" = "Clé publique";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "Clé Publique?";
"Public Key?" = "Clé publique?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Réactions";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relais Recommandés";
"Recommended Relays" = "Relais recommandés";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Refuser";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relais";
/* Sidebar menu label for Relay servers view */
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "Relais";
/* 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!" = "Les relais ont été avisés et les clients pourront utiliser ces renseignements pour filtrer le contenu. Merci!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Tout supprimer";
@@ -357,11 +437,21 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Reply to self" = "Réponse à soi-même";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Répondre à %1$@ & %2$@";
"Replying to %@ & %@" = "Réponse à %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Répondre à:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Signaler";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "ID de signalement:";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Signalement envoyé!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Republier";
@@ -369,12 +459,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "A republié";
/* Navigation bar title for Reposts view. */
"Reposts" = "Republications";
/* 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" = "Demandes";
/* Section title for resetting the user */
"Reset" = "Réinitialiser";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Retenter";
@@ -391,7 +481,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Save" = "Enregistrer";
/* Context menu option to save an image. */
"Save Image" = "Enregistrer Image";
"Save Image" = "Enregistrer l'image";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Rechercher hashtag: #%@";
@@ -411,16 +501,24 @@ 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..." = "Envoyez un message pour démarrer la conversation...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Serveur";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Paramètres";
/* Button to share an image. */
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "Partager";
/* 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" = "Afficher";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "Montrer la clé d'API";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Afficher le sélecteur de portefeuille";
@@ -430,6 +528,12 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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!" = "Merci!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Ils personnifient quelqu'un";
/* 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." = "Il s'agit d'une clé publique, vous ne pourrez pas publier de messages ou interagir de quelque manière que ce soit. Ceci est utilisé pour visualiser les comptes de leur point de vue.";
@@ -440,12 +544,18 @@ Part of a larger sentence to describe how many profiles a user is following. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Ceci est votre identifiant de compte, vous pouvez le donner à vos amis afin qu'ils puissent vous suivre. Cliquez pour copier.";
/* 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!" = "Il s'agit de votre clé de compte secrète. Vous en aurez besoin pour accéder à votre compte. Ne le partagez avec personne ! Enregistrez-le dans un gestionnaire de mots de passe et gardez-le en sécurité!";
"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!" = "Il s'agit de votre clé de compte secrète. Vous en aurez besoin pour accéder à votre compte. Ne le partagez avec personne ! Enregistrez-la dans un gestionnaire de mots de passe et gardez-la en sécurité!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Fil de discussion";
/* Button to translate note from different language. */
"Translate Note" = "Traduire la note";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "Traduit de (lang)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Tapez votre message ici...";
@@ -456,13 +566,22 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Unfollow" = "Se désabonner";
/* 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" = "Ne plus suivre";
"Unfollowing" = "Désabonnement";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Ne plus suivre...";
"Unfollowing..." = "Désabonnement...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Se désabonne de";
"Unfollows" = "Se désabonner";
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Utilisateur bloqué";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "L'utilisateur a été bloqué";
/* Label for Username section of user profile form.
Label to prompt username entry. */
@@ -471,29 +590,38 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Sidebar menu label for Wallet view. */
"Wallet" = "Portefeuille";
/* 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" = "Sélecteur de portefeuille";
/* Label for Website section of user profile form. */
"Website" = "Site Internet";
"Website" = "Site internet";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Bienvenue sur le réseau social %@ contrôle.";
"Welcome to the social network %@ control." = "Bienvenue dans le réseau social sur lequel %@ avez le contrôle.";
/* Text to welcome user. */
"Welcome, %@!" = "Bienvenue, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Que voulez-vous signaler?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://un.relais.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Oui, écraser";
/* 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" = "vous";
/* Label for Your Name section of user profile form. */
"Your Name" = "Votre Nom";
"Your Name" = "Votre nom";
/* 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" = "Votre signalement sera envoyé aux relais auxquels vous êtes connectés";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";

View File

@@ -87,7 +87,7 @@
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Répondre à %@, %@%#@OTHERS@</string>
<string>Réponse à %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
@@ -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>Pourboire</string>
<string>Zap</string>
<key>other</key>
<string>Pourboires</string>
<string>Zaps</string>
</dict>
</dict>
</dict>

View File

@@ -5,5 +5,5 @@
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Dai il permesso a Damus di accedere alle tue Foto per salvare immagini";
"NSPhotoLibraryAddUsageDescription" = "Dai il permesso a Damus di accedere alle tue foto per salvare immagini";

View File

@@ -5,7 +5,7 @@
"'%@' at '%@' will be used for verification" = "'%@' at '%@' sarà usato per la verifica";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' non è un identificatore NIP05 valido. Dovrebbe essere simile ad un indirizzo email.";
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "%@ non è un identificativo NIP-05 valido. Dovrebbe sembrare come un indirizzo email.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Seguaci di (Profile.displayName(profile: profile, pubkey: whos))'";
@@ -16,14 +16,16 @@
/* 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 reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
/* 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 profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "%@ è stato bloccato";
/* 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." = "%@. Per creare un account non hai bisogno di un numero di telefono, un indirizzo email o del tuo nome. Inizia ora senza impegni.";
@@ -33,7 +35,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
/* 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." = "%@. Paga i tuoi amici e accumula sats con Bitcoin⚡, la moneta di internet.";
/* Number of reposts.
/* Number of zap payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
@@ -43,6 +45,9 @@ Number of profiles a user is following. */
/* Placeholder for event mention. */
"< e >" = "< e >";
/* No comment provided by engineer. */
"⚡️ %@" = "⚡️ %@";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Informazioni";
@@ -52,9 +57,15 @@ Number of profiles a user is following. */
/* Placeholder text for About Me description. */
"Absolute Boss" = "Capo supremo";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Accetta";
/* Label to indicate the public ID of the account. */
"Account ID" = "ID dell'account";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Azioni";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Aggiungi";
@@ -63,13 +74,16 @@ Number of profiles a user is following. */
"Add all" = "Aggiungi tutto";
/* Label for section for adding a relay server. */
"Add Relay" = "Aggiungi relè";
"Add Relay" = "Aggiungi Relay";
/* Any amount of sats */
"Any" = "Qualsiasi";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "API Key (facoltativo)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Sei sicuro di voler segnalare questo post?";
"Are you sure you want to repost this?" = "Sei sicuro di voler condividere questo post?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Immagine banner";
@@ -86,6 +100,23 @@ Number of profiles a user is following. */
/* 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" = "Blocca";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "Vuoi bloccare %@?";
/* Title of alert for blocking a user. */
"Block User" = "Blocca Utente";
/* Sidebar menu label for Profile view. */
"Blocked" = "Bloccato";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Utenti bloccati";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
@@ -95,9 +126,12 @@ Number of profiles a user is following. */
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Trasmetti";
/* Button to cancel out of posting a note.
/* 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" = "Annulla";
@@ -111,7 +145,7 @@ Number of profiles a user is following. */
"Clear" = "Cancella";
/* Section title for clearing cached data. */
"Clear Cache" = "Cancella cache";
"Clear Cache" = "Cancella Cache";
/* Label indicating that a user's key was copied. */
"Copied" = "Copiato";
@@ -120,17 +154,17 @@ Number of profiles a user is following. */
"Copy" = "Copia";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copia ID dell'Account";
"Copy Account ID" = "Copia l'ID dell'Account";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copia Immagine";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copia URL dell'Immagine";
"Copy Image URL" = "Copia l'URL dell'immagine";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copia fattura";
"Copy invoice" = "Copia invoice";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copia LNURL";
@@ -141,11 +175,17 @@ Number of profiles a user is following. */
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copia JSON della Nota";
/* Button to copy report ID. */
"Copy Report ID" = "Copia l'ID del Report";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copia Testo";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copia ID dell'Utente";
"Copy User Pubkey" = "Copia la chiave pubblica dell'utente";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Non riesco a trovare l'utente da bloccare...";
/* Button to create account. */
"Create" = "Crea";
@@ -153,17 +193,26 @@ Number of profiles a user is following. */
/* Button to create an account. */
"Create Account" = "Crea Account";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Crea una nuova lista delle persone mutate";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Creatore/i di Bitcoin. Leggenda assoluta";
"Creator(s) of Bitcoin. Absolute legend." = "Il creatore(i) di Bitcoin. Leggenda assoluta";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Custom";
/* 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" = "Portafoglio Principale";
"Default Wallet" = "Portafoglio Predefinito";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Cancella";
/* 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" = "Elimina";
/* Button to dismiss a text field alert. */
"Dismiss" = "Lascia stare";
@@ -171,15 +220,12 @@ Number of profiles a user is following. */
/* Label to prompt display name entry. */
"Display Name" = "Nome visualizzato";
/* 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" = "Tipo DM";
/* 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" = "DM";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Finito";
"Done" = "Fatto";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Guadagna Soldi";
@@ -199,8 +245,8 @@ Number of profiles a user is following. */
/* Error message indicating why saving keys failed. */
"Error: %@" = "Errore: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filtra";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "EULA";
/* Button to follow a user. */
"Follow" = "Segui";
@@ -227,8 +273,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Navigation link to go to profile. */
"Goto profile %@" = "Vai al profilo %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Nascondi";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "Nascondi la chiave dell' API";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Home";
"Home" = "Casa";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://esempio.com/foto.jpg";
@@ -236,9 +288,15 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Contenuto illegale";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Chiave non valida";
/* Button for user to report that the account or content has spam. */
"It's spam" = "E' spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
@@ -248,6 +306,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!" = "Andiamo!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "Traduzioni LibreTranslate";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Indirizzo Lightning o LNURL";
@@ -266,11 +327,11 @@ 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" = "Esci";
/* 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" = "Assicurati di aver salvato la chiave privata (nSEC) prima di uscire o perderai l'accesso a questo account";
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Assicurati di aver salvato la chiave privata prima di uscire o perderai l'accesso a questo account";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
@@ -278,9 +339,15 @@ 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" = "Verifica NIP-05";
/* 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." = "Nessuna lista degli utenti bloccati trovata, vuoi creane una nuova? Creandone una nuova, sovrascriverai eventuali liste di utenti bloccati in precedenza";
/* No search results. */
"none" = "Nessun risultato";
/* Dropdown option for selecting no translation server. */
"None" = "Nessuno";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Niente da vedere qui. Controlla dopo!";
@@ -293,8 +360,11 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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" = "Nudità o contenuti espliciti";
/* Label indicating that a form input is optional. */
"optional" = "opzione1";
"optional" = "facoltativo";
/* Button to pay a Lightning invoice. */
"Pay" = "Paga";
@@ -308,6 +378,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Button to post a note. */
"Post" = "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" = "Post di un utente che hai bloccato";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Post";
@@ -321,7 +394,7 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Private Key" = "Chiave Privata";
/* Sidebar menu label for Profile view. */
"Profile" = "Profilo1";
"Profile" = "Profilo";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Foto Profilo";
@@ -342,13 +415,20 @@ Part of a larger sentence to describe how many profiles a user is following. */
"Reactions" = "Reazioni";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relè consigliati";
"Recommended Relays" = "Relays consigliati";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Rifiuta";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relè";
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relè";
/* 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. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "I relays sono stati notificati e i clients potranno sfruttare questa informazione per filtrare i contenuti. Grazie!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Rimuovi tutto";
@@ -362,19 +442,29 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Rispondi a:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Report";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "ID del Report:";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Report inviato!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Reposta";
"Repost" = "Repost";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Repostato";
/* Navigation bar title for Reposts view. */
"Reposts" = "I Ripost";
/* 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" = "Richiesta";
/* Section title for resetting the user */
"Reset" = "Ricomincia";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Riprova";
@@ -411,16 +501,24 @@ 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..." = "Invia un messaggio e inizia la conversazione...";
/* 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" = "Impostazioni";
/* Button to share an image. */
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "Condividi";
/* 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" = "Mostra";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "Mostra la chiave dell'API";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Mostra wallet disponibili";
@@ -430,22 +528,34 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* 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!" = "Grazie!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Stanno impersonando qualcuno";
/* 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." = "Questa è una chiave pubblica, non potrai postare o interagire in alcun modo. Puoi utilizzarla solo per vedere gli account";
/* 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." = "Questa è una chiave di vecchio tipo. Non siamo sicuri se si tratti di una chiave pubblica o privata. Utilizza il pulsante sottostante se si tratta di una chiave pubblica.";
"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." = "Questa è una chiave nostr vecchia. Non siamo sicuri se si tratti di una chiave pubblica o privata. Utilizza il pulsante sottostante se si tratta di una chiave pubblica.";
/* 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." = "Questo è l'ID del tuo account. Condividilo con i tuoi amici per farti seguire. Clicca per copiare";
/* 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!" = "Questa è la tua chiave privata. Ti serve ad accedere al tuo account. Non condividerla con nessuno! Salvala in un gestore password e tienila al sicuro";
"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!" = "Questa è la tua chiave privata. Ti serve ad accedere al tuo account. Non condividerla mai con nessuno! Salvala in un gestore password e tienila al sicuro";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Thread";
/* Button to translate note from different language. */
"Translate Note" = "Nota sulla traduzione";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (lang)" = "Tradotto da (lang)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Scrivi il tuo post qui...";
@@ -464,6 +574,15 @@ 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" = "Smetti di seguire";
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Utente bloccato";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "L'utente è stato bloccato";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nome utente";
@@ -471,8 +590,8 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Sidebar menu label for Wallet view. */
"Wallet" = "Portafoglio";
/* 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" = "Seleziona un portafoglio";
@@ -486,8 +605,14 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Text to welcome user. */
"Welcome, %@!" = "Benvenuto, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Che cosa vuoi segnalare?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://un.relè.com";
"wss://some.relay.com" = "wss://qualche.relay.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Si, sovrascrivi";
/* 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" = "tu";
@@ -495,6 +620,9 @@ Part of a larger sentence to describe how many profiles a user is following. */
/* Label for Your Name section of user profile form. */
"Your Name" = "Nome";
/* 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" = "Questo report sarà inviato a i relays a cui sei connesso";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";

View File

@@ -11,9 +11,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d other note</string>
<string>%d altra nota</string>
<key>other</key>
<string>%d other notes</string>
<string>%d altre note</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
@@ -27,9 +27,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Follower</string>
<string>Seguace</string>
<key>other</key>
<string>Followers</string>
<string>Seguaci</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
@@ -45,9 +45,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Reaction</string>
<string>Reazione</string>
<key>other</key>
<string>Reactions</string>
<string>Reazioni</string>
</dict>
</dict>
<key>relays_count</key>
@@ -61,15 +61,15 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Relay</string>
<string>Relè</string>
<key>other</key>
<string>Relays</string>
<string>Relè</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@%#@OTHERS@</string>
<string>Rispondendo a %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
@@ -77,9 +77,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> &amp; %d other</string>
<string> &amp; %d altro</string>
<key>other</key>
<string> &amp; %d others</string>
<string> &amp; %d altri</string>
<key>zero</key>
<string></string>
</dict>
@@ -87,7 +87,7 @@
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@, %@%#@OTHERS@</string>
<string>Rispondendo a %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
@@ -95,9 +95,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> &amp; %d other</string>
<string> &amp; %d altro</string>
<key>other</key>
<string> &amp; %d others</string>
<string> &amp; %d altri</string>
<key>zero</key>
<string></string>
</dict>
@@ -115,7 +115,7 @@
<key>one</key>
<string>Repost</string>
<key>other</key>
<string>Reposts</string>
<string>I Repost</string>
</dict>
</dict>
<key>sats_count</key>
@@ -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>Tip</string>
<string>Zap</string>
<key>other</key>
<string>Tips</string>
<string>Zaps</string>
</dict>
</dict>
</dict>

View File

@@ -5,5 +5,5 @@
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Piešķir Damus piekļuvi saviem fotoattēliem, lai varat saglabāt attēlus.";
"NSPhotoLibraryAddUsageDescription" = "Atļauj Damus piekļuvi saviem fotoattēliem, lai varat saglabāt attēlus.";

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