Compare commits

...

251 Commits

Author SHA1 Message Date
Vitor Pamplona
227de74317 v0.77.0 2023-09-18 13:36:51 -04:00
Vitor Pamplona
0b9fced8bc Quick refactoring to abstract the name Amber for any external signers. 2023-09-18 13:36:36 -04:00
Vitor Pamplona
274e4f7498 Fixes merge issue on NIP24 TextNotes 2023-09-18 11:23:36 -04:00
Vitor Pamplona
f6496b5e7e Merge pull request #601 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-18 11:18:57 -04:00
Crowdin Bot
ee5ff1845f New Crowdin translations by GitHub Action 2023-09-18 15:18:18 +00:00
Vitor Pamplona
c84e835ce5 Merge pull request #543 from greenart7c3/amber
Add Amber support
2023-09-18 11:16:55 -04:00
Vitor Pamplona
cdb5be2fe9 Merge branch 'main' into amber 2023-09-18 11:16:47 -04:00
Vitor Pamplona
2d0c356aa5 v0.76.2 2023-09-18 10:53:44 -04:00
Vitor Pamplona
0a92c253b8 Merge pull request #600 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-18 10:39:22 -04:00
Crowdin Bot
92347863bc New Crowdin translations by GitHub Action 2023-09-18 14:38:52 +00:00
Vitor Pamplona
a146ec5e1a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-18 10:36:21 -04:00
Vitor Pamplona
83be43e94e Removes the ZapSplit display when the weights are zero or were incorrectly created. 2023-09-18 10:36:05 -04:00
greenart7c3
d726b3b5b5 fix createZapRequestFor userhex 2023-09-18 11:27:05 -03:00
Vitor Pamplona
c845aac8e5 Merge pull request #599 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-17 12:03:08 -04:00
Crowdin Bot
1cdf114fff New Crowdin translations by GitHub Action 2023-09-17 15:38:29 +00:00
Vitor Pamplona
4d1a99d076 Adds NIP24 Kind1 create function 2023-09-17 11:32:35 -04:00
Vitor Pamplona
b1debd9879 AnimatedVisibility seems faster than Crossfade 2023-09-17 11:31:24 -04:00
Vitor Pamplona
3843917bd1 Makes sure the account is writeable before signing Auth for notifications 2023-09-16 15:33:42 -04:00
greenart7c3
2fa4281b6f fix sending messages 2023-09-16 09:11:25 -03:00
greenart7c3
c42944e15b removed AmberUtils.signEvent 2023-09-16 08:49:46 -03:00
greenart7c3
9f9f158709 fix zap splits and show message if user uninstall amber 2023-09-16 08:23:56 -03:00
greenart7c3
3a753f0d5d Merge branch 'main' into amber 2023-09-16 08:03:54 -03:00
greenart7c3
13c94112d4 fix private zaps 2023-09-16 07:56:14 -03:00
greenart7c3
dd46f12a05 change amber from debug to release 2023-09-15 21:51:01 -03:00
greenart7c3
9a648d7e84 send the logged in npub to amber 2023-09-15 21:35:32 -03:00
greenart7c3
614dbab1fd refactor some duplicated code 2023-09-15 20:51:13 -03:00
greenart7c3
3d80ee1984 fix decryption of dms whe new message is send 2023-09-15 20:46:29 -03:00
Vitor Pamplona
536fb49bc3 0.76.1 2023-09-15 17:15:54 -04:00
Vitor Pamplona
edd93141bc No need for the @ when adding users to the Split 2023-09-15 17:13:35 -04:00
Vitor Pamplona
20c2d19a9c Refining the Payment via Intent 2023-09-15 17:08:19 -04:00
Vitor Pamplona
17b2fe9cd8 Merge pull request #596 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-15 14:22:41 -04:00
Crowdin Bot
134e7cee0c New Crowdin translations by GitHub Action 2023-09-15 16:59:32 +00:00
Vitor Pamplona
108ddffcc7 formatting 2023-09-15 12:56:50 -04:00
Vitor Pamplona
20e76daff8 Adds zap splits 2023-09-15 12:56:24 -04:00
greenart7c3
6876a7f0d2 zaps in background 2023-09-15 13:53:05 -03:00
greenart7c3
87dbf86129 Merge branch 'main' into amber 2023-09-15 13:18:43 -03:00
greenart7c3
175d1ae33b fix zap dialog not closing 2023-09-15 12:58:58 -03:00
greenart7c3
bf9d318209 use content resolvers with amber 2023-09-15 11:56:07 -03:00
Vitor Pamplona
7bee44c5ad Merge pull request #595 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-14 14:01:54 -04:00
Crowdin Bot
a32e90aed0 New Crowdin translations by GitHub Action 2023-09-14 17:48:43 +00:00
Vitor Pamplona
310db1008e Fixes bug when verifying the checksum of LNURLs (bech32) in uppercase. 2023-09-14 13:46:29 -04:00
greenart7c3
21560d098e fix upload image 2023-09-13 19:15:00 -03:00
greenart7c3
33f7c5c82e deleted intentutils 2023-09-13 18:06:50 -03:00
greenart7c3
52f17e9f46 nwc support with amber 2023-09-13 16:23:56 -03:00
greenart7c3
1e7ffa365b change relay auth to use signevent 2023-09-13 16:08:38 -03:00
greenart7c3
e00fc83e5c fix reaction row and zaps 2023-09-13 15:54:44 -03:00
greenart7c3
a2fcb3539e Merge branch 'main' into amber 2023-09-13 13:15:45 -03:00
Vitor Pamplona
cb69d056b8 Allowing playback to save any position and not only after 5 seconds. 2023-09-13 12:12:47 -04:00
Vitor Pamplona
e7f950cd32 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-13 11:45:27 -04:00
Vitor Pamplona
3b2686ffeb Removes potential bug when the list contains an e tag with an a reference 2023-09-13 11:45:17 -04:00
Vitor Pamplona
2387a609df Merge pull request #593 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-13 10:33:47 -04:00
Crowdin Bot
6457a84f4b New Crowdin translations by GitHub Action 2023-09-13 14:30:48 +00:00
Vitor Pamplona
dee231d43a Moves deletion and report to be the last item on the dropdown menu: https://image.nostr.build/709c4f2753cf6a40f9f9322379394d09354acb7e88502b64be348e4a3030734b.png 2023-09-13 10:27:37 -04:00
greenart7c3
8c4af4ac85 only decrypt giftwrap in the dms screen 2023-09-13 11:10:10 -03:00
greenart7c3
58cf0d9f5d fix freeze when going to dms screen 2023-09-13 10:08:43 -03:00
greenart7c3
de5c0d4681 do not try decrypt private zaps for now 2023-09-13 09:32:24 -03:00
greenart7c3
780832d785 fix block/unblock 2023-09-13 09:22:34 -03:00
greenart7c3
b8808a17b5 fix bookmarks decryption 2023-09-13 08:38:20 -03:00
greenart7c3
5001e58c2b improve giftwrap decryption 2023-09-13 08:04:51 -03:00
greenart7c3
95514b66ea improve giftwrap decryption 2023-09-13 08:01:46 -03:00
greenart7c3
90c6d6c8f8 fix login with amber 2023-09-13 06:42:37 -03:00
Vitor Pamplona
88d2ce3ad0 Updating dependencies 2023-09-12 17:53:18 -04:00
Vitor Pamplona
484f3a6842 Merge pull request #590 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-12 08:32:16 -04:00
Crowdin Bot
866caf8bc5 New Crowdin translations by GitHub Action 2023-09-12 12:12:41 +00:00
Vitor Pamplona
be9e3c3bf7 Merge pull request #589 from davotoula/main
Updated translations for cs/de/sv
2023-09-12 08:11:02 -04:00
David Kaspar
49a3cb1978 Updated translations for cs/de/sv 2023-09-12 11:41:43 +02:00
greenart7c3
c3ddc4f7ad fix release build not working 2023-09-11 18:12:55 -03:00
greenart7c3
ff4d704bde add support for private zaps 2023-09-11 15:30:00 -03:00
greenart7c3
a773be12c4 decrypt LnZapRequestEvent 2023-09-11 11:52:53 -03:00
greenart7c3
7ff6f16df0 Merge branch 'main' into amber 2023-09-11 07:20:03 -03:00
Vitor Pamplona
d9053f580f Merge pull request #586 from OrigiSize/patch-1
Minor README.md edits
2023-09-09 19:37:57 -04:00
OrigiSize
fee2764338 Minor README.md edits
Fixing grammar and flow
2023-09-09 12:33:09 -07:00
Vitor Pamplona
61cb4fdd74 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-09 14:42:59 -04:00
Vitor Pamplona
d89869ebff Merge pull request #585 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-09 14:40:45 -04:00
Vitor Pamplona
8518819dfa Forces valid hexes for users and notes 2023-09-09 14:39:41 -04:00
Crowdin Bot
0a8923be6b New Crowdin translations by GitHub Action 2023-09-09 18:34:33 +00:00
Vitor Pamplona
cda821e5af Stops accepting space as a Valid hex char and requires an even number of chars (padding) 2023-09-09 14:33:19 -04:00
Vitor Pamplona
be2402702f Forces the ZapRequest to exist when processing the LnZapEvent. 2023-09-09 14:32:27 -04:00
Vitor Pamplona
d723a8ab5c Refactors the split between nip19 and the rest of the text. 2023-09-09 14:26:20 -04:00
Vitor Pamplona
5c882c81c8 Account is never null here. 2023-09-09 14:24:29 -04:00
Vitor Pamplona
24d2709564 Merge pull request #584 from atrifat/move-broadcast-button-away
Fix vitorpamplona/amethyst#550 - change broadcast button position
2023-09-08 20:54:46 -04:00
Rif'at Ahdi R
ce15bc46a7 Fix vitorpamplona/amethyst#550 - change broadcast button position 2023-09-09 08:24:17 +08:00
Vitor Pamplona
de4bc30730 Merge pull request #583 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-08 16:19:48 -04:00
Crowdin Bot
64f6811c00 New Crowdin translations by GitHub Action 2023-09-08 20:18:47 +00:00
Vitor Pamplona
e05ab104b9 0.76.0 2023-09-08 16:16:53 -04:00
Vitor Pamplona
b4f1c4d13e Migrates to Encrypted Push Notifications 2023-09-08 16:16:26 -04:00
greenart7c3
36a3c4123c Merge branch 'main' into amber 2023-09-08 15:48:13 -03:00
Vitor Pamplona
939eb1bd8d Fixes the lack of permission when using Android's PhotoPicker to upload videos (thumbnail fails to load and videos don't play because the Playback service doesn't have permission to run) 2023-09-08 11:39:14 -04:00
Vitor Pamplona
854469cc7d Merge pull request #582 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-08 09:21:01 -04:00
Crowdin Bot
338ae41baa New Crowdin translations by GitHub Action 2023-09-08 12:42:58 +00:00
Vitor Pamplona
6cae89d276 Adds indonesian to crowdin 2023-09-08 08:41:07 -04:00
Vitor Pamplona
15c1e25122 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-08 08:37:20 -04:00
Vitor Pamplona
c5716416b6 Merge pull request #581 from atrifat/add-indonesian-translation
Add Indonesian Translation
2023-09-08 08:36:56 -04:00
Rif'at Ahdi R
65e4a766ab Add Indonesian Translation 2023-09-08 11:39:27 +08:00
Vitor Pamplona
c2a4c9bbb3 Improvements to the relay settings page. 2023-09-07 19:07:33 -04:00
Vitor Pamplona
b042110154 Merge pull request #579 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-07 15:39:07 -04:00
Crowdin Bot
da97b5593b New Crowdin translations by GitHub Action 2023-09-07 18:42:45 +00:00
Vitor Pamplona
44b4b0cdb9 Makes sure only valid lnurl withdraws get highlighted. 2023-09-07 14:40:42 -04:00
Vitor Pamplona
190b7fe819 Using the correct observer for picture profile in the Top bar. 2023-09-07 12:53:19 -04:00
Vitor Pamplona
8eb89f0fe9 Adds nostr.wine and noswhere.com as two new NIP-50 relays. 2023-09-07 12:52:44 -04:00
Vitor Pamplona
3085353729 Adds SeenOn relays when successfully broadcasting a note. 2023-09-07 12:52:14 -04:00
Vitor Pamplona
1465ce112c Refines relay icon row compose 2023-09-07 11:11:10 -04:00
Vitor Pamplona
5c797858ba Merge pull request #578 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-07 10:23:12 -04:00
Crowdin Bot
8c51c4be0c New Crowdin translations by GitHub Action 2023-09-07 13:57:52 +00:00
Vitor Pamplona
4e8ecff67b Fixes the missing channel header in conversations. 2023-09-07 09:55:43 -04:00
greenart7c3
6303573dd9 support for sending and receiving nip 44 dms 2023-09-06 18:53:11 -03:00
Vitor Pamplona
83f8c51aca Merge pull request #577 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-06 10:14:04 -04:00
Crowdin Bot
e675348461 New Crowdin translations by GitHub Action 2023-09-06 14:04:55 +00:00
Vitor Pamplona
9026f5bf07 Merge pull request #574 from davotoula/main
Updated translations for cs/de
2023-09-06 10:03:30 -04:00
greenart7c3
a7aa3705ef fix crash on reaction 2023-09-06 10:43:59 -03:00
greenart7c3
a64c941f36 add cache to decrypted dms 2023-09-06 09:21:55 -03:00
David Kaspar
02f18b5b35 Updated translations for cs/de 2023-09-06 13:35:45 +02:00
Vitor Pamplona
db740f4747 Upgrades the RelayPoolStatus to a Flow 2023-09-05 17:21:36 -04:00
Vitor Pamplona
6a3206b938 Avoids race condition when update EOSEs 2023-09-05 17:20:37 -04:00
Vitor Pamplona
23414b0bc6 Merge pull request #573 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-05 09:39:11 -04:00
Crowdin Bot
0234cfca30 New Crowdin translations by GitHub Action 2023-09-05 13:03:08 +00:00
Vitor Pamplona
f707714611 Merge pull request #569 from atrifat/add-arm64-armeabi-target
Add arm64-v8a and armeabi-v7a build target
2023-09-05 09:01:46 -04:00
Vitor Pamplona
7b90230c45 Merge pull request #572 from davotoula/main
Updated translations for cs/de
2023-09-05 09:01:05 -04:00
David Kaspar
d3468f3eba Added new translations for cs/de 2023-09-05 12:52:59 +02:00
Rif'at Ahdi R
fc9500b45c Update create-release.yml to suppport arm64-v8a and armeabi-v7a 2023-09-05 10:39:36 +08:00
Rif'at Ahdi R
df0b5fcd19 Add arm64-v8a and armeabi-v7a build target 2023-09-05 10:03:35 +08:00
greenart7c3
59cde5699e add support for status 2023-09-04 18:28:11 -03:00
greenart7c3
bd836a9580 Merge branch 'main' into amber 2023-09-04 18:09:29 -03:00
greenart7c3
90aade338c sign event in background 2023-09-04 18:06:09 -03:00
Vitor Pamplona
40e2dba490 v0.75.14 2023-09-02 13:43:15 -04:00
Vitor Pamplona
d13e8bacec Fixes thread ordering issues with two notes with the same idHex 2023-09-02 12:46:24 -04:00
Vitor Pamplona
d3fa05a4df Remeber should be based on the Note 2023-09-02 12:28:16 -04:00
Vitor Pamplona
734dd2e119 Adds UI Feedback for Custom Zaps 2023-09-02 12:27:24 -04:00
Vitor Pamplona
a9ea9ea2ae Improves error message from LN Invoice servers. 2023-09-02 12:26:36 -04:00
Vitor Pamplona
1b521bc140 Grammar adjustment 2023-09-02 12:25:41 -04:00
Vitor Pamplona
364e3c5f5f 0.75.13 2023-09-01 14:28:40 -04:00
Vitor Pamplona
647cda3f90 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-01 14:27:52 -04:00
Vitor Pamplona
bcca4d4bcc Adds idHex to the date sorting in replies and comments to make sure the order is correct when 2 posts are placed within the same second. 2023-09-01 14:25:48 -04:00
Vitor Pamplona
0b038dca17 Merge pull request #565 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-01 14:10:07 -04:00
Crowdin Bot
ad6f63df56 New Crowdin translations by GitHub Action 2023-09-01 17:53:20 +00:00
Vitor Pamplona
b5b8b62a16 Only trigger notification of events within 5minutes from the current time (avoids annoying notifications when rebroadcasting events) 2023-09-01 13:51:23 -04:00
Vitor Pamplona
8d3066b377 Marks which relays are using compression 2023-09-01 12:36:19 -04:00
Vitor Pamplona
18933b4326 Ignores brb.io if in the relay list. It's crashing the app too much with a weird implementation of the WebSockets protocol 2023-09-01 12:36:03 -04:00
Vitor Pamplona
672841c0ed Doesn't notify if the GiftWrap's inner message came from the user. 2023-09-01 12:35:21 -04:00
Vitor Pamplona
872c7cb257 Fixes testing of thread ordering 2023-09-01 12:34:46 -04:00
greenart7c3
6d691c4741 add support for reports 2023-09-01 13:32:39 -03:00
greenart7c3
bbba27752d support for public and anon zaps 2023-09-01 12:25:42 -03:00
greenart7c3
d9cf090cc9 add support for nip94 2023-09-01 10:46:50 -03:00
greenart7c3
46effa572c refactor default values in signEvent variables 2023-09-01 10:18:49 -03:00
greenart7c3
f76ab01c11 Merge branch 'main' into amber 2023-09-01 05:12:29 -03:00
Vitor Pamplona
bfae5871c8 v0.75.12 2023-08-31 19:05:12 -04:00
Vitor Pamplona
451c5d1648 Removing LiveData Redirections 2023-08-31 19:04:07 -04:00
Vitor Pamplona
3423626c60 Fixes global feed 2023-08-31 19:02:54 -04:00
Vitor Pamplona
8982f02a4a v0.75.11 2023-08-31 17:13:32 -04:00
Vitor Pamplona
f06b5fdacf New thread ordering scheme:
1. Thread author, ascending order by date.
2. My replies, descending by date.
3. My follow's replies, descending by date.
4. Everybody else, descending by date.
2023-08-31 16:41:02 -04:00
Vitor Pamplona
8ea8cf2985 Marks keep screen on to false when leaving a video player. 2023-08-31 15:23:30 -04:00
Vitor Pamplona
2771d34951 Fixes the report card when the author has been blocked by the logged in user but the list loaded before the block user list. 2023-08-31 14:53:43 -04:00
Vitor Pamplona
669b33a3e1 Reducing the height of the banner in the drawer 2023-08-31 13:43:57 -04:00
Vitor Pamplona
c934a1cdfe Reduce the space between display name and status. 2023-08-31 13:36:46 -04:00
Vitor Pamplona
0ccd41f5e8 Fixes wrong EOSE updates. 2023-08-31 13:21:08 -04:00
Vitor Pamplona
1af3bb7dfc Improve cohesiveness of the pruning system. 2023-08-31 11:48:22 -04:00
Vitor Pamplona
f0fcf1ea8a Fixes the grammar in the nip05 error messages 2023-08-31 09:04:09 -04:00
Vitor Pamplona
5325542c81 update vico 2023-08-30 19:56:37 -04:00
Vitor Pamplona
3d9675841b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-08-30 19:32:31 -04:00
Vitor Pamplona
0d475fba0e Merge pull request #564 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-30 17:31:45 -04:00
Crowdin Bot
05714de945 New Crowdin translations by GitHub Action 2023-08-30 21:14:24 +00:00
Vitor Pamplona
89c1b53c95 Merge pull request #563 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-30 17:13:08 -04:00
greenart7c3
d1cb699027 support for nip95 2023-08-30 16:40:11 -03:00
greenart7c3
9ee1250f3a unblock users 2023-08-30 14:15:20 -03:00
greenart7c3
7a68cf867e block users using amber 2023-08-30 11:54:36 -03:00
Vitor Pamplona
1c9041f12b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-08-30 10:12:23 -04:00
Crowdin Bot
d3de5e0a42 New Crowdin translations by GitHub Action 2023-08-30 12:50:26 +00:00
Vitor Pamplona
b1bd21533d Merge pull request #562 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-30 08:49:11 -04:00
Crowdin Bot
51f997a34a New Crowdin translations by GitHub Action 2023-08-30 12:48:43 +00:00
Vitor Pamplona
d5607faadd Requires 10 sat zaps to trigger notifications 2023-08-30 08:47:25 -04:00
Vitor Pamplona
1a9c3ccb79 Must have a valid ZapRequest to process. 2023-08-30 08:44:14 -04:00
greenart7c3
bc8fa00608 move openAmber to AmberUtils 2023-08-30 08:29:56 -03:00
greenart7c3
329d806ffd decrypt dms of notifications 2023-08-30 08:07:45 -03:00
greenart7c3
e040d0f971 Merge branch 'main' into amber 2023-08-30 07:03:55 -03:00
Vitor Pamplona
053de4d06f Removes unnecessary checks since the dual LiveData structure was activated 2023-08-29 17:57:47 -04:00
Vitor Pamplona
4016458540 v0.75.10 2023-08-29 17:40:51 -04:00
Vitor Pamplona
8b98efaad7 Refines zap and report pruning. 2023-08-29 17:24:48 -04:00
Vitor Pamplona
217ed90d9a Fix the deletion of reports from authors 2023-08-29 17:14:25 -04:00
Vitor Pamplona
b82aefbd5d Merge pull request #559 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-29 16:54:41 -04:00
Crowdin Bot
63b954d38d New Crowdin translations by GitHub Action 2023-08-29 20:54:17 +00:00
Vitor Pamplona
76cdea0087 Fixing xml 2023-08-29 16:53:09 -04:00
Vitor Pamplona
da1a3a8f85 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-08-29 16:51:05 -04:00
Vitor Pamplona
d11e92e166 v0.75.9 2023-08-29 16:46:34 -04:00
Vitor Pamplona
61efd237c2 Fixes: https://github.com/vitorpamplona/amethyst/issues/495 and https://github.com/vitorpamplona/amethyst/issues/398 2023-08-29 16:46:27 -04:00
Vitor Pamplona
6099948605 Migrates from Toast Message to a Dialog when Zap fails in the reaction row. 2023-08-29 16:43:45 -04:00
Vitor Pamplona
45b743e7c8 Merge pull request #557 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-29 16:42:50 -04:00
Crowdin Bot
108c6cbeb4 New Crowdin translations by GitHub Action 2023-08-28 21:18:41 +00:00
Vitor Pamplona
da02891c6a Correct an xml issue from crowdin 2023-08-28 17:12:48 -04:00
Vitor Pamplona
a03e9ea946 Merge pull request #556 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-08-28 16:59:51 -04:00
Crowdin Bot
dd4a6f8da0 New Crowdin translations by GitHub Action 2023-08-28 20:57:58 +00:00
Vitor Pamplona
c01033cbb0 Fixes case 2023-08-28 16:56:52 -04:00
Vitor Pamplona
1fe93395c5 gotta choose one 2023-08-28 16:51:05 -04:00
Vitor Pamplona
cf960fe59d Avoid creating english files when they have not been translated yet. 2023-08-28 16:49:12 -04:00
Vitor Pamplona
2416d41131 removes filter to only run crowdin when strings are updated 2023-08-28 16:41:53 -04:00
Vitor Pamplona
0699095d77 triggers crowdin 2023-08-28 16:32:00 -04:00
Vitor Pamplona
6ed3e84d95 This might work 2023-08-28 16:31:09 -04:00
Vitor Pamplona
6c196cfeb7 triggering crowdin 2023-08-28 16:27:26 -04:00
Vitor Pamplona
987784fb89 Fixing yaml again 2023-08-28 16:26:59 -04:00
Vitor Pamplona
d60ac5d73b updates strings 2023-08-28 16:24:04 -04:00
Vitor Pamplona
c2361c034c corrects yaml 2023-08-28 16:23:51 -04:00
Vitor Pamplona
199514e7f3 trigger crowdin 2023-08-28 16:22:07 -04:00
Vitor Pamplona
a1364d19c2 correctly identifies mapping languages. 2023-08-28 16:21:47 -04:00
Vitor Pamplona
e73428b156 updates to strings.xml 2023-08-28 16:09:28 -04:00
Vitor Pamplona
a684a222cb runs github action on any updates to source or dst translations 2023-08-28 16:08:32 -04:00
Vitor Pamplona
1646cbbd68 mapping language codes to crowdin 2023-08-28 16:07:26 -04:00
Vitor Pamplona
035a48a0cd adds a type android 2023-08-28 15:55:32 -04:00
Vitor Pamplona
71be8ce239 testing update to strings.xml 2023-08-28 15:43:49 -04:00
Vitor Pamplona
1ccf9bf467 only runs with strings change 2023-08-28 15:14:47 -04:00
Vitor Pamplona
42a0cd0b1b adding the configuration file for crowdin 2023-08-28 15:12:39 -04:00
Vitor Pamplona
39547b1d11 Adds Crowin 2023-08-28 14:43:22 -04:00
greenart7c3
c223c1372b add support for amber in the follow dropdown menu 2023-08-28 14:31:20 -03:00
greenart7c3
a450dcbac1 add support for add/remove public bookmarks 2023-08-28 11:58:48 -03:00
greenart7c3
ee5fbce2ce refactor mainactivity to use IntentUtils.start 2023-08-28 09:56:12 -03:00
greenart7c3
559b88be18 fix build after merge 2023-08-28 07:18:04 -03:00
greenart7c3
dfbc5fa556 Merge branch 'main' into amber 2023-08-28 07:06:52 -03:00
greenart7c3
df1a017eb3 refactor PackageUtils.isAmberInstalled to account.loggedinwithamber 2023-08-25 11:51:22 -03:00
greenart7c3
b2b7799a0f add support for deletion events 2023-08-25 09:29:13 -03:00
greenart7c3
e817f94045 add/remove private bookmarks 2023-08-25 08:51:23 -03:00
greenart7c3
46571a6029 decrypt private bookmark 2023-08-25 06:56:49 -03:00
greenart7c3
4f4d689f5a fix build, use keyPair in textnoteevent, check if loggedInWithAmber in newpostview 2023-08-25 05:45:39 -03:00
greenart7c3
a0308938de Merge branch 'main' into amber 2023-08-25 05:27:14 -03:00
greenart7c3
b10e875941 created loggedInWithAmber in accountViewModel, changed some events to use keypair 2023-08-23 15:56:31 -03:00
greenart7c3
632400f52c add login with amber button 2023-08-23 13:43:25 -03:00
greenart7c3
b63479c45f fix build 2023-08-23 07:33:49 -03:00
greenart7c3
7ea6495f28 Merge branch 'main' into amber 2023-08-23 07:28:58 -03:00
greenart7c3
453e63025d fix crash in PushNotificationUtils when theres no google play services 2023-08-21 15:58:01 -03:00
greenart7c3
17ecc98e1e Merge branch 'main' into amber 2023-08-21 14:59:20 -03:00
greenart7c3
f9272f5cfc implemented amber support for auth event and the notifications 2023-08-21 14:48:11 -03:00
greenart7c3
348e07a307 fix missing navScrollToTop parameter 2023-08-21 09:00:45 -03:00
greenart7c3
2009f5bd7b Merge branch 'main' into amber 2023-08-21 08:50:56 -03:00
greenart7c3
7ea92bc068 send dms using amber 2023-08-18 10:57:17 -03:00
greenart7c3
3b07b5d8dd Refactor SignerDialog to use the rawJson and type 2023-08-18 07:55:14 -03:00
greenart7c3
f06e79cfd3 Merge branch 'main' into amber 2023-08-18 06:33:02 -03:00
greenart7c3
584ceb1f0f Merge branch 'main' into amber 2023-08-16 17:21:47 -03:00
greenart7c3
8053119d42 decrypt dms using amber 2023-08-16 16:39:42 -03:00
greenart7c3
b8e97525eb add signer support whem replying 2023-08-16 14:34:11 -03:00
greenart7c3
66b1e4b15d add signer dialog to communities and public chats 2023-08-14 16:34:40 -03:00
greenart7c3
e6f6f64512 add signer dialog in the relay screen 2023-08-14 13:53:10 -03:00
greenart7c3
d2eea9f0ae fix merge 2023-08-14 10:51:14 -03:00
greenart7c3
012fdc8cc6 add signer support in the user metadata screen 2023-08-14 10:46:03 -03:00
greenart7c3
74054aab5a add signer support for sending notes 2023-08-14 10:46:02 -03:00
greenart7c3
11908b9e0c if amber is installed show floatingActionButton 2023-08-14 10:43:10 -03:00
greenart7c3
570cccfc62 add signer support in the profile tabs 2023-08-14 10:42:57 -03:00
greenart7c3
de9d6877e1 fix merge 2023-08-14 10:42:57 -03:00
greenart7c3
c31fa116ca add signerdialog to boosts 2023-08-14 10:42:57 -03:00
greenart7c3
10b15226a1 added SignerDialog in the like reaction 2023-08-14 10:42:57 -03:00
greenart7c3
7a46376bc1 added SignerDialog in the hashtagscreen 2023-08-14 10:42:57 -03:00
greenart7c3
b761aaf970 sign unfollow event 2023-08-14 10:42:57 -03:00
greenart7c3
3a08db2dfb add signerDialog in the profile screen follow button 2023-08-14 10:42:57 -03:00
greenart7c3
41fd2e087c use rememberLauncherForActivityResult to send the intent and get the result back 2023-08-14 10:42:57 -03:00
greenart7c3
ba476bcdca created SignerDialog 2023-08-14 10:42:56 -03:00
189 changed files with 10903 additions and 3404 deletions

View File

@@ -112,6 +112,28 @@ jobs:
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload Play APK arm64-v8a Asset
id: upload-release-asset-play-arm64-v8a-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload Play APK armeabi-v7a Asset
id: upload-release-asset-play-armeabi-v7a-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
# F-Droid APK
- name: Upload F-Droid APK Universal Asset
id: upload-release-asset-fdroid-universal-apk
@@ -146,6 +168,28 @@ jobs:
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload F-Droid APK arm64-v8a Asset
id: upload-release-asset-fdroid-arm64-v8a-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload F-Droid APK armeabi-v7a Asset
id: upload-release-asset-fdroid-armeabi-v7a-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
# Google Play AAB

30
.github/workflows/crowdin.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Crowdin Action
on:
push:
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
branches: [ main ]
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: crowdin action
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: true
download_translations: true
localization_branch_name: l10n_crowdin_translations
create_pull_request: true
pull_request_title: 'New Crowdin Translations'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'main'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -0,0 +1,32 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -106,7 +106,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
This is a native Android app made with Kotlin and Jetpack Compose.
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
The overall architecture consists in the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
@@ -172,7 +172,7 @@ For the Play build:
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
```
2. Create 4 Secret Key variables on your GitHub repository and fill in with the signing key information
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
- `KEY_ALIAS` <- `<alias_name>`
- `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>`
@@ -192,15 +192,15 @@ The relay also learns which public keys you are requesting, meaning your public
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
# DM Privacy #
While the content of direct messages (DMs) is only visible to you, and your DM nostr counterparty, everyone can see that and when you and your counterparty are DM-ing each other.
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
# Visibility & Permanence of Your Content on nostr
## Information Visibility ##
Content that you share can be shared to other relays.
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
# Screenshots

View File

@@ -13,14 +13,14 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 287
versionName "0.75.8"
versionCode 297
versionName "0.77.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'in', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
}
buildTypes {
@@ -54,7 +54,7 @@ android {
abi {
enable true
reset()
include "x86", "x86_64"
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
universalApk true
}
}
@@ -112,7 +112,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Zoomable images
implementation "net.engawapg.lib:zoomable:1.5.0"
implementation 'net.engawapg.lib:zoomable:1.5.1'
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"

View File

@@ -1,8 +1,10 @@
package com.vitorpamplona.amethyst
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.Event
import junit.framework.TestCase
@@ -95,7 +97,7 @@ class ThreadAssemblerTest {
val naddr = ATag(30023, "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", "1680612926599", null)
val filter = ThreadFeedFilter(naddr.toTag())
val filter = ThreadFeedFilter(Account(KeyPair()), naddr.toTag())
val calculatedFeed = filter.feed()
val expecteedOrder = listOf(
@@ -126,14 +128,14 @@ class ThreadAssemblerTest {
"6a58f8315af5badb1bdaeb5489417b94621a4d8e192ae2fedcca0c5dcf0c9cd4",
"6e9bb03c7c40d67fec0d0bb872548ec207ba0ac4533efa137d7bcaca9fb4b191",
"e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3",
"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7",
"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3",
"674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d",
"d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9",
"00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031",
"87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8",
"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e",
"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883",
"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3",
"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7",
"d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5",
"8cdc4676aca93bbafcfbe6784f9b2df54e8ca20fbe69ba55fda487736bfdb7f6",
"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26"

View File

@@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications
import com.vitorpamplona.amethyst.AccountInfo
class PushNotificationUtils {
fun init(accounts: List<AccountInfo>) {
object PushNotificationUtils {
var hasInit: Boolean = true
suspend fun init(accounts: List<AccountInfo>) {
}
}

View File

@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
<package android:name="com.greenart7c3.nostrsigner"/>
<package android:name="com.greenart7c3.nostrsigner.debug"/>
</queries>
<!-- Doesn't require a camera -->

View File

@@ -35,7 +35,8 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean = false
val hasPrivKey: Boolean,
val loggedInWithExternalSigner: Boolean
)
private object PrefKeys {
@@ -74,6 +75,7 @@ private object PrefKeys {
const val THEME = "theme"
const val PREFERRED_LANGUAGE = "preferred_Language"
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
@@ -82,7 +84,7 @@ object LocalPreferences {
private var _currentAccount: String? = null
private fun currentAccount(): String? {
fun currentAccount(): String? {
if (_currentAccount == null) {
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
@@ -201,7 +203,11 @@ object LocalPreferences {
fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts().map { npub ->
AccountInfo(npub = npub)
AccountInfo(
npub,
hasPrivKey(npub),
getLoggedInWithExternalSigner(npub)
)
}
}
@@ -245,6 +251,7 @@ object LocalPreferences {
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
}.apply()
val globalPrefs = encryptedPreferences()
@@ -276,17 +283,19 @@ object LocalPreferences {
}
fun getTheme(): Int {
encryptedPreferences().apply {
return getInt(PrefKeys.THEME, 0)
}
return encryptedPreferences().getInt(PrefKeys.THEME, 0)
}
fun getPreferredLanguage(): String {
var language = ""
encryptedPreferences().apply {
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
return language
return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
private fun getLoggedInWithExternalSigner(npub: String): Boolean {
return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
}
private fun hasPrivKey(npub: String): Boolean {
return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
}
fun loadFromEncryptedStorage(): Account? {
@@ -363,6 +372,7 @@ object LocalPreferences {
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
@@ -431,7 +441,8 @@ object LocalPreferences {
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
settings = settings
settings = settings,
loginWithExternalSigner = loginWithExternalSigner
)
return a

View File

@@ -92,9 +92,7 @@ class MultiPlayerPlaybackManager(
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
STATE_IDLE -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
cachedPositions.add(uri, player.currentPosition)
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
@@ -104,9 +102,7 @@ class MultiPlayerPlaybackManager(
}
}
else -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
cachedPositions.add(uri, player.currentPosition)
}
}
}

View File

@@ -12,6 +12,7 @@ import coil.disk.DiskCache
import coil.util.DebugLogger
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
@@ -35,15 +36,21 @@ import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import java.io.File
object ServiceManager {
var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
private var account: Account? = null
fun start(account: Account, context: Context) {
this.account = account
ExternalSignerUtils.account = account
start(context)
}
@Synchronized
fun start(context: Context) {
if (isStarted && account != null) {
return
}
Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account
@@ -88,6 +95,7 @@ object ServiceManager {
NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start()
isStarted = true
}
}
@@ -113,6 +121,7 @@ object ServiceManager {
NostrVideoDataSource.stop()
Client.disconnect()
isStarted = false
}
fun cleanObservers() {

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
@@ -11,6 +13,7 @@ import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
@@ -53,6 +56,10 @@ object LocalCache {
// checkNotInMainThread()
return users[key] ?: run {
require(isValidHex(key = key)) {
"$key is not a valid hex"
}
val newObject = User(key)
users.putIfAbsent(key, newObject) ?: newObject
}
@@ -98,6 +105,10 @@ object LocalCache {
checkNotInMainThread()
return notes.get(idHex) ?: run {
require(isValidHex(idHex)) {
"$idHex is not a valid hex"
}
val newObject = Note(idHex)
notes.putIfAbsent(idHex, newObject) ?: newObject
}
@@ -198,6 +209,14 @@ object LocalCache {
val user = getOrCreateUser(event.pubKey)
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
if (event.dTag() == "bookmark") {
val loggedInUser = LocalPreferences.currentAccount()
val hexKey = loggedInUser?.bechToBytes()
if (hexKey != null) {
val pubKey = Hex.encode(hexKey)
if (pubKey == event.pubKey) {
ExternalSignerUtils.content.remove(event.id)
}
}
user.updateBookmark(event)
}
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
@@ -807,23 +826,23 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) }
if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
(
(zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
)
note.loadEvent(event, author, repliesTo)
if (zapRequest == null) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
// Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
repliesTo.forEach {
@@ -1183,13 +1202,7 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
// Doesn't need to clean up the replies and mentions.. Too small to matter.
// Counts the replies
it.replyTo?.forEach { parent ->
parent.removeReply(it)
}
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
@@ -1208,12 +1221,7 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
// Counts the replies
it.replyTo?.forEach { parent ->
parent.removeReply(it)
}
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
@@ -1240,21 +1248,12 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
val newerVersion = addressables[(it.event as? AddressableEvent)?.address()?.toTag()]
if (newerVersion != null) {
it.moveAllReferencesTo(newerVersion)
}
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.clearEOSE() // allows reloading of these events
}
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
@@ -1274,6 +1273,7 @@ object LocalCache {
it.value.event is ReactionEvent || it.value.event is LnZapEvent || it.value.event is LnZapRequestEvent ||
it.value.event is ReportEvent || it.value.event is GenericRepostEvent
) &&
it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
it.value.liveSet?.isInUse() != true && // don't delete if observing.
it.value.author?.pubkeyHex !in accounts && // don't delete if it is the logged in account
it.value.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user
@@ -1282,27 +1282,88 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
masterNote.clearEOSE() // allows reloading of these events
}
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
removeChildrenOf(childrenToBeRemoved)
toBeRemoved.forEach {
it.replyTo?.forEach { masterNote ->
masterNote.clearEOSE() // allows reloading of these events
}
}
if (toBeRemoved.size > 1) {
println("PRUNE: ${toBeRemoved.size} thread replies removed.")
}
}
private fun removeFromCache(note: Note) {
note.replyTo?.forEach { masterNote ->
masterNote.removeReply(note)
masterNote.removeBoost(note)
masterNote.removeReaction(note)
masterNote.removeZap(note)
masterNote.removeReport(note)
masterNote.clearEOSE() // allows reloading of these events if needed
}
if (note.event is LnZapEvent) {
(note.event as LnZapEvent).zappedAuthor().mapNotNull {
val author = getUserIfExists(it)
author?.removeZap(note)
author?.clearEOSE()
}
}
if (note.event is LnZapRequestEvent) {
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull {
val author = getUserIfExists(it)
author?.removeZap(note)
author?.clearEOSE()
}
}
if (note.event is ReportEvent) {
(note.event as ReportEvent).reportedAuthor().mapNotNull {
val author = getUserIfExists(it.key)
author?.removeReport(note)
author?.clearEOSE()
}
}
notes.remove(note.idHex)
}
fun removeAuthorLinkTo(note: Note) {
if (note.event is LnZapEvent) {
(note.event as LnZapEvent).zappedAuthor().mapNotNull {
val author = getUserIfExists(it)
author?.removeZap(note)
author?.clearEOSE()
}
}
if (note.event is LnZapRequestEvent) {
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull {
val author = getUserIfExists(it)
author?.removeZap(note)
author?.clearEOSE()
}
}
if (note.event is ReportEvent) {
(note.event as ReportEvent).reportedAuthor().mapNotNull {
val author = getUserIfExists(it.key)
author?.removeReport(note)
author?.clearEOSE()
}
}
}
fun removeChildrenOf(nextToBeRemoved: List<Note>) {
nextToBeRemoved.forEach { note ->
removeFromCache(note)
}
}
fun pruneExpiredEvents() {
checkNotInMainThread()
@@ -1313,17 +1374,7 @@ object LocalCache {
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
masterNote.clearEOSE() // allows reloading of these events
}
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
@@ -1350,17 +1401,7 @@ object LocalCache {
}.flatten()
toBeRemoved.forEach {
// Counts the replies
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
}
notes.remove(it.idHex)
removeFromCache(it)
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
@@ -1369,21 +1410,6 @@ object LocalCache {
println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden")
}
fun removeChildrenOf(nextToBeRemoved: List<Note>) {
nextToBeRemoved.forEach { note ->
if (note.event is LnZapEvent) {
(note.event as LnZapEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) }
}
if (note.event is LnZapRequestEvent) {
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) }
}
if (note.event is ReportEvent) {
(note.event as ReportEvent).reportedAuthor().mapNotNull { getUserIfExists(it.key)?.removeZap(note) }
}
notes.remove(note.idHex)
}
}
fun pruneContactLists(loggedIn: Set<HexKey>) {
checkNotInMainThread()
@@ -1476,9 +1502,10 @@ object LocalCache {
is LiveActivitiesChatMessageEvent -> consume(event, relay)
is LnZapEvent -> {
event.zapRequest?.let {
// must have a valid request
verifyAndConsume(it, relay)
consume(event)
}
consume(event)
}
is LnZapRequestEvent -> consume(event)
is LnZapPaymentRequestEvent -> consume(event)

View File

@@ -136,26 +136,60 @@ open class Note(val idHex: String) {
.format(DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss"))
}
data class LevelSignature(val signature: String, val createdAt: Long?, val author: User?)
/**
* This method caches signatures during each execution to avoid recalculation in longer threads
*/
fun replyLevelSignature(
eventsToConsider: Set<Note>,
cachedSignatures: MutableMap<Note, String>
): String {
eventsToConsider: Set<HexKey>,
cachedSignatures: MutableMap<Note, LevelSignature>,
account: User,
accountFollowingSet: Set<String>,
now: Long
): LevelSignature {
val replyTo = replyTo
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
return LevelSignature(
signature = "/" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) + ";",
createdAt = createdAt(),
author = author
)
}
val mySignature = (
val parent = (
replyTo
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
.filter { it.idHex in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
.map {
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
cachedSignatures[it] ?: it.replyLevelSignature(
eventsToConsider,
cachedSignatures,
account,
accountFollowingSet,
now
).apply { cachedSignatures.put(it, this) }
}
.maxByOrNull { it.length }?.removeSuffix(";") ?: ""
) + "/" + formattedDateTime(createdAt() ?: 0) + ";"
.maxByOrNull { it.signature.length }
)
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
val threadOrder = if (parent?.author == author && createdAt() != null) {
// author of the thread first, in **ascending** order
"9" + formattedDateTime((parent?.createdAt ?: 0) + (now - (createdAt() ?: 0))) + idHex.substring(0, 8)
} else if (author?.pubkeyHex == account.pubkeyHex) {
"8" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my replies
} else if (author?.pubkeyHex in accountFollowingSet) {
"7" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my follows replies.
} else {
"0" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // everyone else.
}
val mySignature = LevelSignature(
signature = parentSignature + "/" + threadOrder + ";",
createdAt = createdAt(),
author = author
)
cachedSignatures[this] = mySignature
return mySignature
@@ -700,6 +734,12 @@ class NoteLiveSet(u: Note) {
it.note.boosts.toImmutableList()
}.distinctUntilChanged()
val relayInfo = innerRelays.map {
it.note.relays.map {
RelayBriefInfo(it)
}.toImmutableList()
}
fun isInUse(): Boolean {
return metadata.hasObservers() ||
reactions.hasObservers() ||
@@ -737,16 +777,12 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
}
fun invalidateData() {
if (!hasObservers()) return
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(NoteState(note))
}
postValue(NoteState(note))
}
}
@@ -782,3 +818,10 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
@Immutable
class NoteState(val note: Note)
@Immutable
data class RelayBriefInfo(
val url: String,
val displayUrl: String = url.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/").intern(),
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
)

View File

@@ -14,4 +14,6 @@ data class RelaySetupInfo(
val spamCount: Int = 0,
val feedTypes: Set<FeedType>,
val paidRelay: Boolean = false
)
) {
val briefInfo: RelayBriefInfo = RelayBriefInfo(url)
}

View File

@@ -5,16 +5,15 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChatroomKey
@@ -27,9 +26,6 @@ import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import java.math.BigDecimal
import java.util.regex.Pattern
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
@Stable
class User(val pubkeyHex: String) {
@@ -104,6 +100,10 @@ class User(val pubkeyHex: String) {
liveSet?.innerBookmarks?.invalidateData()
}
fun clearEOSE() {
latestEOSEs = emptyMap()
}
fun updateContactList(event: ContactListEvent) {
if (event.id == latestContactList?.id) return
@@ -264,19 +264,11 @@ class User(val pubkeyHex: String) {
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
try {
val url = String(Bech32.decodeBytes(newUserInfo.lud06!!, false).second)
val matcher = lnurlpPattern.matcher(url)
while (matcher.find()) {
val domain = matcher.group(2)
val username = matcher.group(3)
info?.lud16 = "$username@$domain"
if (newUserInfo.lud16.isNullOrBlank()) {
info?.lud06?.let {
if (it.lowercase().startsWith("lnurl")) {
info?.lud16 = Lud06().toLud16(it)
}
} catch (t: Throwable) {
// Doesn't create errors.
}
}
@@ -465,15 +457,12 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
}
fun invalidateData() {
if (!hasObservers()) return
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(UserState(user))
}
postValue(UserState(user))
}
}

View File

@@ -0,0 +1,307 @@
package com.vitorpamplona.amethyst.service
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.util.LruCache
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
enum class SignerType {
SIGN_EVENT,
NIP04_ENCRYPT,
NIP04_DECRYPT,
NIP44_ENCRYPT,
NIP44_DECRYPT,
GET_PUBLIC_KEY,
DECRYPT_ZAP_EVENT
}
object ExternalSignerUtils {
val content = LruCache<String, String>(10)
var isActivityRunning: Boolean = false
val cachedDecryptedContent = mutableMapOf<HexKey, String>()
lateinit var account: Account
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var decryptResultLauncher: ActivityResultLauncher<Intent>
private lateinit var blockListResultLauncher: ActivityResultLauncher<Intent>
@OptIn(DelicateCoroutinesApi::class)
fun requestRejectedToast() {
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(
Amethyst.instance,
Amethyst.instance.getString(R.string.sign_request_rejected),
Toast.LENGTH_SHORT
).show()
}
}
@OptIn(DelicateCoroutinesApi::class)
fun default() {
isActivityRunning = false
ServiceManager.shouldPauseService = true
GlobalScope.launch(Dispatchers.IO) {
isActivityRunning = false
ServiceManager.shouldPauseService = true
}
}
@OptIn(DelicateCoroutinesApi::class)
fun start(activity: MainActivity) {
activityResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val event = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
content.put(id, event)
}
}
default()
}
decryptResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val event = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
content.put(id, event)
cachedDecryptedContent[id] = event
}
}
default()
}
blockListResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
cachedDecryptedContent[id] = decryptedContent
account.live.invalidateData()
}
}
default()
}
}
@OptIn(DelicateCoroutinesApi::class)
fun openSigner(
data: String,
type: SignerType,
intentResult: ActivityResultLauncher<Intent>,
pubKey: HexKey,
id: String
) {
try {
ServiceManager.shouldPauseService = false
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
val signerType = when (type) {
SignerType.SIGN_EVENT -> "sign_event"
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
SignerType.GET_PUBLIC_KEY -> "get_public_key"
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
}
intent.putExtra("type", signerType)
intent.putExtra("pubKey", pubKey)
intent.putExtra("id", id)
if (type !== SignerType.GET_PUBLIC_KEY) {
intent.putExtra("current_user", account.keyPair.pubKey.toNpub())
}
intent.`package` = "com.greenart7c3.nostrsigner"
intentResult.launch(intent)
} catch (e: Exception) {
Log.e("Signer", "Error opening Signer app", e)
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(
Amethyst.instance,
Amethyst.instance.getString(R.string.error_opening_external_signer),
Toast.LENGTH_SHORT
).show()
}
}
}
fun openSigner(event: EventInterface, columnName: String = "signature") {
checkNotInMainThread()
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
if (result !== null) {
content.put(event.id(), result)
return
}
ServiceManager.shouldPauseService = false
isActivityRunning = true
openSigner(
event.toJson(),
SignerType.SIGN_EVENT,
activityResultLauncher,
"",
event.id()
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
blockListResultLauncher,
pubKey,
id
)
}
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) {
data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray()
} else {
data
}
Amethyst.instance.contentResolver.query(
Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"),
localData,
null,
null,
null
).use {
if (it !== null) {
if (it.moveToFirst()) {
val index = it.getColumnIndex(columnName)
return it.getString(index)
}
}
}
return null
}
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
// do nothing
}
}
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
return
}
isActivityRunning = true
openSigner(
decryptedContent,
signerType,
activityResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptZapEvent(event: LnZapRequestEvent) {
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
if (result !== null) {
content.put(event.id, result)
cachedDecryptedContent[event.id] = result
return
}
openSigner(
event.toJson(),
SignerType.DECRYPT_ZAP_EVENT,
decryptResultLauncher,
event.pubKey,
event.id
)
}
}

View File

@@ -47,13 +47,13 @@ class Nip05Verifier() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not resolve $nip05. Error: ${it.code}. Check if the server up and if the address $nip05 is correct")
onError("Could not resolve $nip05. Error: ${it.code}. Check if the server is up and if the address $nip05 is correct")
}
}
}
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not resolve $url. Check if the server up and if the address $nip05 is correct")
onError("Could not resolve $url. Check if the server is up and if the address $nip05 is correct")
e.printStackTrace()
}
})

View File

@@ -0,0 +1,103 @@
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import com.vitorpamplona.amethyst.model.RelayInformation
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
object Nip11CachedRetriever {
val relayInformationDocumentCache = LruCache<String, RelayInformation>(100)
val retriever = Nip11Retriever()
suspend fun loadRelayInfo(
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit
) {
val url = retriever.cleanUrl(dirtyUrl)
val doc = relayInformationDocumentCache.get(url)
if (doc != null) {
onInfo(doc)
} else {
Nip11Retriever().loadRelayInfo(
url,
dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, it)
onInfo(it)
},
onError
)
}
}
}
class Nip11Retriever {
enum class ErrorCode {
FAIL_TO_ASSEMBLE_URL,
FAIL_TO_REACH_SERVER,
FAIL_TO_PARSE_RESULT,
FAIL_WITH_HTTP_STATUS
}
fun cleanUrl(dirtyUrl: String): String {
return if (dirtyUrl.contains("://")) {
dirtyUrl
.replace("wss://", "https://")
.replace("ws://", "http://")
} else {
"https://$dirtyUrl"
}
}
suspend fun loadRelayInfo(
url: String,
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onError: (String, ErrorCode, String?) -> Unit
) {
try {
val request: Request = Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClient.getHttpClient()
.newCall(request)
.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
checkNotInMainThread()
response.use {
val body = it.body.string()
try {
if (it.isSuccessful) {
onInfo(RelayInformation.fromJson(body))
} else {
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
}
} catch (e: Exception) {
Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
}
}
}
override fun onFailure(call: Call, e: IOException) {
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
}
}
)
} catch (e: Exception) {
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
}
}
}

View File

@@ -8,17 +8,26 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrAccountDataSource : NostrDataSource("AccountData") {
@@ -143,6 +152,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
event.cachedGift(privateKey)?.let {
this.consume(it, relay)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGift(account.keyPair.pubKey, cached)?.let {
this.consume(it, relay)
}
}
}
@@ -152,6 +175,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
event.cachedGossip(privateKey)?.let {
LocalCache.justConsume(it, relay)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
LocalCache.justConsume(it, relay)
}
}
// Don't store sealed gossips to avoid rebroadcasting by mistake.
@@ -181,7 +218,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
if (this::account.isInitialized) {
val event = account.createAuthEvent(relay, challenge)
if (event != null) {
Client.send(
event,

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.Error
abstract class NostrDataSource(val debugName: String) {
@@ -23,6 +24,7 @@ abstract class NostrDataSource(val debugName: String) {
data class Counter(var counter: Int)
private var eventCounter = mapOf<String, Counter>()
var changingFilters = AtomicBoolean()
fun printCounter() {
eventCounter.forEach {
@@ -49,7 +51,7 @@ abstract class NostrDataSource(val debugName: String) {
// Log.e("ERROR", "Relay ${relay.url}: ${error.message}")
}
override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
override fun onRelayStateChange(type: Relay.Type, relay: Relay, subscriptionId: String?) {
// Log.d("RELAY", "Relay ${relay.url} ${when (type) {
// Relay.Type.CONNECT -> "connected."
// Relay.Type.DISCONNECT -> "disconnected."
@@ -57,13 +59,19 @@ abstract class NostrDataSource(val debugName: String) {
// Relay.Type.EOSE -> "sent all events it had stored."
// }}")
if (type == Relay.Type.EOSE && channel != null) {
if (type == Relay.Type.EOSE && subscriptionId != null && subscriptionId in subscriptions.keys) {
// updates a per subscripton since date
subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
subscriptions[subscriptionId]?.updateEOSE(
TimeUtils.fiveMinutesAgo(), // in case people's clock is slighly off.
relay.url
)
}
}
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
if (success) {
markAsSeenOnRelay(eventId, relay)
}
}
override fun onAuth(relay: Relay, challenge: String) {
@@ -136,6 +144,8 @@ abstract class NostrDataSource(val debugName: String) {
// saves the current content to only update if it changes
val currentFilters = activeSubscriptions.associate { it.id to it.toJson() }
changingFilters.getAndSet(true)
updateChannelFilters()
// Makes sure to only send an updated filter when it actually changes.
@@ -167,12 +177,18 @@ abstract class NostrDataSource(val debugName: String) {
}
}
}
changingFilters.getAndSet(false)
}
open fun consume(event: Event, relay: Relay) {
LocalCache.verifyAndConsume(event, relay)
}
open fun markAsSeenOnRelay(eventId: String, relay: Relay) {
LocalCache.getNoteIfExists(eventId)?.addRelay(relay)
}
abstract fun updateChannelFilters()
open fun auth(relay: Relay, challenge: String) = Unit
}

View File

@@ -54,8 +54,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
}
fun createFollowAccountsFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followSet = follows.plus(account.userProfile().pubkeyHex).toList()
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
return TypedFilter(
types = setOf(FeedType.FOLLOWS),

View File

@@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource(
override fun auth(relay: Relay, challenge: String) {
super.auth(relay, challenge)
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey)
val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
Client.send(
event,
relay.url

View File

@@ -134,6 +134,9 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
}
val singleEventChannel = requestNewChannel { time, relayUrl ->
// Ignores EOSE if it is in the middle of a filter change.
if (changingFilters.get()) return@requestNewChannel
checkNotInMainThread()
eventsToWatch.forEach {

View File

@@ -0,0 +1,17 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
object PackageUtils {
private fun isPackageInstalled(context: Context, target: String): Boolean {
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
}
fun isOrbotInstalled(context: Context): Boolean {
return isPackageInstalled(context, "org.torproject.android")
}
fun isAmberInstalled(context: Context): Boolean {
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
}
}

View File

@@ -0,0 +1,207 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.round
class ZapPaymentHandler(val account: Account) {
@Immutable
data class Payable(
val info: ZapSplitSetup,
val user: User?,
val amountMilliSats: Long,
val invoice: String
)
suspend fun zap(
note: Note,
amountMilliSats: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType
) = withContext(Dispatchers.IO) {
val zapSplitSetup = note.event?.zapSplitSetup()
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else {
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) {
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
return@withContext
}
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
val totalWeight = zapsToSend.sumOf { it.weight }
val invoicesToPayOnIntent = mutableListOf<Payable>()
zapsToSend.forEachIndexed { index, value ->
val outerProgressMin = index / zapsToSend.size.toFloat()
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
val zapValue =
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
if (value.isLnAddress) {
innerZap(
lud16 = value.lnAddressOrPubKeyHex,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(
Payable(
info = value,
user = null,
amountMilliSats = zapValue,
invoice = it
)
)
}
)
} else {
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
val lud16 = user?.info?.lnAddress()
if (lud16 != null) {
innerZap(
lud16 = lud16,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
overrideUser = user,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(
Payable(
info = value,
user = user,
amountMilliSats = zapValue,
invoice = it
)
)
}
)
} else {
onError(
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
)
)
}
}
}
if (invoicesToPayOnIntent.isNotEmpty()) {
onPayViaIntent(invoicesToPayOnIntent.toImmutableList())
onProgress(1f)
} else {
launch(Dispatchers.IO) {
// Awaits for the event to come back to LocalCache.
delay(5000)
onProgress(1f)
}
}
}
private suspend fun innerZap(
lud16: String,
note: Note,
amount: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayInvoiceThroughIntent: (String) -> Unit,
zapType: LnZapEvent.ZapType,
overrideUser: User? = null
) {
var zapRequestJson = ""
if (zapType != LnZapEvent.ZapType.NONZAP) {
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
if (zapRequest != null) {
zapRequestJson = zapRequest.toJson()
}
}
onProgress(0.10f)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount,
message,
zapRequestJson,
onSuccess = {
onProgress(0.7f)
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(
bolt11 = it,
note,
onResponse = { response ->
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
onError(
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
)
} else {
onProgress(1f)
}
}
)
onProgress(0.8f)
} else {
try {
onPayInvoiceThroughIntent(it)
} catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found2))
}
onProgress(0f)
}
},
onError = onError,
onProgress = onProgress
)
}
}

View File

@@ -4,16 +4,14 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal
import java.math.RoundingMode
import java.net.URLEncoder
class LightningAddressResolver() {
@@ -27,11 +25,7 @@ class LightningAddressResolver() {
}
if (lnaddress.lowercase().startsWith("lnurl")) {
return try {
String(Bech32.decodeBytes(lnaddress, false).second)
} catch (e: Exception) {
null
}
return Lud06().toLnUrlp(lnaddress)
}
return null
@@ -57,16 +51,16 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server up and if the lightning address is correct")
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct")
}
}
} catch (e: Exception) {
e.printStackTrace()
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
onError("Could not resolve $url. Check if the server is up and if the lightning address $lnaddress is correct")
}
}
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
@@ -82,22 +76,13 @@ class LightningAddressResolver() {
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) {
onSuccess(response.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
}
}
client.newCall(request).execute().use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
}
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
e.printStackTrace()
}
})
}
}
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
@@ -115,7 +100,7 @@ class LightningAddressResolver() {
milliSats: Long,
message: String,
nostrRequest: String? = null,
onSuccess: (String) -> Unit,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit
) {
@@ -159,14 +144,22 @@ class LightningAddressResolver() {
lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr ->
// Forces LN Invoice amount to be the requested amount.
val expectedAmountInSats = BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) {
if (invoiceAmount.toLong() == expectedAmountInSats) {
onProgress(0.7f)
onSuccess(pr)
} else {
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server")
onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
}
} ?: onError("Invoice Not Created (element pr not found in the resulting JSON)")
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
onProgress(0.0f)
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
} ?: run {
onProgress(0.0f)
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
}
},
onError = onError
)

View File

@@ -7,6 +7,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.SignerType
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
import com.vitorpamplona.amethyst.ui.note.showAmount
@@ -19,43 +21,127 @@ import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.persistentSetOf
import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) {
suspend fun consume(event: GiftWrapEvent) {
if (!LocalCache.justVerify(event)) return
if (!notificationManager().areNotificationsEnabled()) return
suspend fun consume(event: Event) {
if (LocalCache.notes[event.id] == null) {
if (LocalCache.justVerify(event)) {
LocalCache.justConsume(event, null)
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) {
consumeIfMatchesAccount(event, acc)
}
}
}
val manager = notificationManager()
if (manager.areNotificationsEnabled()) {
when (event) {
is PrivateDmEvent -> notify(event)
is LnZapEvent -> notify(event)
is GiftWrapEvent -> unwrapAndNotify(event)
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
val key = account.keyPair.privKey
if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
pushWrappedEvent.content,
pushWrappedEvent.pubKey,
pushWrappedEvent.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: ""
}
pushWrappedEvent.unwrap(cached)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
} else if (key != null) {
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
}
}
suspend fun unwrapAndConsume(event: Event, account: Account): Event? {
private fun unwrapAndConsume(event: Event, account: Account): Event? {
if (!LocalCache.justVerify(event)) return null
return when (event) {
is GiftWrapEvent -> {
val key = account.keyPair.privKey ?: return null
event.cachedGift(key)?.let {
unwrapAndConsume(it, account)
val key = account.keyPair.privKey
if (key != null) {
event.cachedGift(key)?.let {
unwrapAndConsume(it, account)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGift(account.keyPair.pubKey, cached)?.let {
unwrapAndConsume(it, account)
}
} else {
null
}
}
is SealedGossipEvent -> {
val key = account.keyPair.privKey ?: return null
event.cachedGossip(key)?.let {
// this is not verifiable
LocalCache.justConsume(it, null)
it
val key = account.keyPair.privKey
if (key != null) {
event.cachedGossip(key)?.let {
// this is not verifiable
LocalCache.justConsume(it, null)
it
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
LocalCache.justConsume(it, null)
it
}
} else {
null
}
}
else -> {
@@ -65,106 +151,106 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
private suspend fun unwrapAndNotify(giftWrap: GiftWrapEvent) {
val giftWrapNote = LocalCache.notes[giftWrap.id] ?: return
private fun notify(event: ChatMessageEvent, acc: Account) {
if (event.createdAt > TimeUtils.fiveMinutesAgo() && // old event being re-broadcasted
event.pubKey != acc.userProfile().pubkeyHex
) { // from the user
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
val chatNote = LocalCache.notes[event.id] ?: return
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
if (acc != null && acc.userProfile().pubkeyHex == giftWrap.recipientPubKey()) {
val chatEvent = unwrapAndConsume(giftWrap, account = acc)
val followingKeySet = acc.followingKeySet()
if (chatEvent is ChatMessageEvent && acc.keyPair.privKey != null) {
val chatNote = LocalCache.notes[chatEvent.id] ?: return
val chatRoom = chatEvent.chatroomKey(acc.keyPair.pubKey.toHexKey())
val isKnownRoom = (
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
val followingKeySet = acc.followingKeySet()
val isKnownRoom = (
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
val noteUri = chatNote.toNEvent()
notificationManager().sendDMNotification(chatEvent.id, content, user, userPicture, noteUri, applicationContext)
}
}
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
val noteUri = chatNote.toNEvent()
notificationManager().sendDMNotification(
event.id,
content,
user,
userPicture,
noteUri,
applicationContext
)
}
}
}
private fun notify(event: PrivateDmEvent) {
private fun notify(event: PrivateDmEvent, acc: Account) {
val note = LocalCache.notes[event.id] ?: return
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
// old event being re-broadcast
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
val followingKeySet = acc.followingKeySet()
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
val followingKeySet = acc.followingKeySet()
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
}.toSet()
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
}.toSet()
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
val content = acc.decryptContent(note) ?: ""
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
}
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
val content = acc.decryptContent(note) ?: ""
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
}
}
}
}
private fun notify(event: LnZapEvent) {
private fun notify(event: LnZapEvent, acc: Account) {
val noteZapEvent = LocalCache.notes[event.id] ?: return
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) }
// old event being re-broadcast
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) }
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
if (acc != null && acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
val amount = showAmount(event.amount)
val senderInfo = (noteZapRequest?.event as? LnZapRequestEvent)?.let {
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
if (decryptedContent != null) {
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
Pair(author, decryptedContent.content)
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
Pair(noteZapRequest.author, noteZapRequest.event?.content())
} else {
Pair(noteZapRequest.author, null)
}
if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
val amount = showAmount(event.amount)
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
if (decryptedContent != null) {
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
Pair(author, decryptedContent.content)
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
Pair(noteZapRequest.author, noteZapRequest.event?.content())
} else {
Pair(noteZapRequest.author, null)
}
val zappedContent =
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
val user = senderInfo?.first?.toBestDisplayName() ?: ""
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
senderInfo?.second?.ifBlank { null }?.let {
title += " ($it)"
}
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
zappedContent?.let {
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
}
val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications"
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
}
val zappedContent =
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
val user = senderInfo?.first?.toBestDisplayName() ?: ""
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
senderInfo?.second?.ifBlank { null }?.let {
title += " ($it)"
}
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
zappedContent?.let {
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
}
val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications"
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
}
}

View File

@@ -23,7 +23,7 @@ class RegisterAccounts(
): List<RelayAuthEvent> {
return accounts.mapNotNull {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null) {
if (acc != null && acc.isWriteable()) {
val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
if (relayToUse != null) {
acc.createAuthEvent(relayToUse, notificationToken)
@@ -36,7 +36,7 @@ class RegisterAccounts(
}
}
private fun postRegistrationEvent(events: List<RelayAuthEvent>) {
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
try {
val jsonObject = """{
"events": [ ${events.joinToString(", ") { it.toJson() }} ]

View File

@@ -16,9 +16,7 @@ object Constants {
}
val defaultRelays = arrayOf(
// Free relays for DMs and Follows
RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.snort.social", read = true, write = true, feedTypes = activeTypes),
// Free relays for only DMs and Follows due to the amount of spam
RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes),
// Chats
@@ -40,6 +38,7 @@ object Constants {
// NewRelayListViewModel.Relay("wss://brb.io", read = true, write = true, feedTypes = activeTypes),
// Paid relays
RelaySetupInfo("wss://relay.snort.social", read = true, write = false, feedTypes = activeTypesGlobalChats),
RelaySetupInfo("wss://relay.nostr.com.au", read = true, write = false, feedTypes = activeTypesGlobalChats),
RelaySetupInfo("wss://eden.nostr.land", read = true, write = false, feedTypes = activeTypesGlobalChats),
RelaySetupInfo("wss://nostr.milou.lol", read = true, write = false, feedTypes = activeTypesGlobalChats),
@@ -51,8 +50,15 @@ object Constants {
RelaySetupInfo("wss://relay.nostrati.com", read = true, write = false, feedTypes = activeTypesGlobalChats),
// Supporting NIP-50
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch)
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://filter.nostr.wine", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch)
)
val forcedRelayForSearch = RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch)
val forcedRelayForSearch = arrayOf(
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://filter.nostr.wine", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch)
)
val forcedRelaysForSearchSet = forcedRelayForSearch.map { it.url }
}

View File

@@ -47,6 +47,7 @@ class Relay(
private var listeners = setOf<Listener>()
private var socket: WebSocket? = null
private var isReady: Boolean = false
private var usingCompression: Boolean = false
var eventDownloadCounterInBytes = 0
var eventUploadCounterInBytes = 0
@@ -85,6 +86,9 @@ class Relay(
private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) {
// BRB is crashing OkHttp Deflater object :(
if (url.contains("brb.io")) return
// If there is a connection, don't wait.
if (connectingBlock.getAndSet(true)) {
return
@@ -115,7 +119,10 @@ class Relay(
override fun onOpen(webSocket: WebSocket, response: Response) {
checkNotInMainThread()
markConnectionAsReady(response.receivedResponseAtMillis - response.sentRequestAtMillis)
markConnectionAsReady(
pingInMs = response.receivedResponseAtMillis - response.sentRequestAtMillis,
usingCompression = response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false
)
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
onConnected(this@Relay)
@@ -175,15 +182,17 @@ class Relay(
}
}
fun markConnectionAsReady(pingInMs: Long) {
fun markConnectionAsReady(pingInMs: Long, usingCompression: Boolean) {
this.afterEOSE = false
this.isReady = true
this.pingInMs = pingInMs
this.usingCompression = usingCompression
}
fun markConnectionAsClosed() {
this.socket = null
this.isReady = false
this.usingCompression = false
this.afterEOSE = false
this.closingTimeInSeconds = TimeUtils.now()
}
@@ -239,6 +248,7 @@ class Relay(
socket?.close(1000, "Normal close")
socket = null
isReady = false
usingCompression = false
afterEOSE = false
}

View File

@@ -1,9 +1,13 @@
package com.vitorpamplona.amethyst.service.relays
import androidx.lifecycle.LiveData
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
/**
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
@@ -12,6 +16,11 @@ object RelayPool : Relay.Listener {
private var relays = listOf<Relay>()
private var listeners = setOf<Listener>()
// Backing property to avoid flow emissions from other classes
private var _lastStatus = RelayPoolStatus(0, 0)
private val _statusFlow = MutableSharedFlow<RelayPoolStatus>(1, 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val statusFlow: SharedFlow<RelayPoolStatus> = _statusFlow.asSharedFlow()
fun availableRelays(): Int {
return relays.size
}
@@ -76,11 +85,13 @@ object RelayPool : Relay.Listener {
fun addRelay(relay: Relay) {
relay.register(this)
relays += relay
updateStatus()
}
fun removeRelay(relay: Relay) {
relay.unregister(this)
relays = relays.minus(relay)
updateStatus()
}
fun register(listener: Listener) {
@@ -109,12 +120,14 @@ object RelayPool : Relay.Listener {
override fun onError(relay: Relay, subscriptionId: String, error: Error) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
refreshObservers()
updateStatus()
}
override fun onRelayStateChange(relay: Relay, type: Relay.Type, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
refreshObservers()
if (type != Relay.Type.EOSE) {
updateStatus()
}
}
override fun onSendResponse(relay: Relay, eventId: String, success: Boolean, message: String) {
@@ -125,18 +138,15 @@ object RelayPool : Relay.Listener {
listeners.forEach { it.onAuth(relay, challenge) }
}
// Observers line up here.
val live: RelayPoolLiveData = RelayPoolLiveData(this)
private fun refreshObservers() {
live.refresh()
private fun updateStatus() {
val connected = connectedRelays()
val available = availableRelays()
if (_lastStatus.connected != connected || _lastStatus.available != available) {
_lastStatus = RelayPoolStatus(connected, available)
_statusFlow.tryEmit(_lastStatus)
}
}
}
class RelayPoolLiveData(val relays: RelayPool) : LiveData<RelayPoolState>(RelayPoolState(relays)) {
fun refresh() {
postValue(RelayPoolState(relays))
}
}
class RelayPoolState(val relays: RelayPool)
@Immutable
data class RelayPoolStatus(val connected: Int, val available: Int, val isConnected: Boolean = connected > 0)

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
@@ -22,6 +23,7 @@ import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
@@ -50,6 +52,8 @@ import java.nio.charset.StandardCharsets
class MainActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
ExternalSignerUtils.start(this)
super.onCreate(savedInstanceState)
LocalPreferences.migrateSingleUserPrefs()
@@ -65,7 +69,10 @@ class MainActivity : AppCompatActivity() {
themeViewModel.onChange(LocalPreferences.getTheme())
AmethystTheme(themeViewModel) {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val accountStateViewModel: AccountStateViewModel = viewModel {
AccountStateViewModel(this@MainActivity)
}
@@ -81,7 +88,8 @@ class MainActivity : AppCompatActivity() {
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
val connectivityManager =
getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.requestNetwork(networkRequest, networkCallback)
}
@@ -94,11 +102,13 @@ class MainActivity : AppCompatActivity() {
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.start(this@MainActivity)
if (ServiceManager.shouldPauseService) {
ServiceManager.start(this@MainActivity)
}
}
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
}
}
@@ -108,7 +118,9 @@ class MainActivity : AppCompatActivity() {
debugState(this)
// }
ServiceManager.pause()
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
super.onPause()
}
@@ -140,8 +152,10 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
}
}
}
@@ -153,7 +167,8 @@ class MainActivity : AppCompatActivity() {
super.onCapabilitiesChanged(network, networkCapabilities)
GlobalScope.launch(Dispatchers.IO) {
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasMobileData =
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
@@ -170,7 +185,9 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
}
}
}
@@ -178,9 +195,15 @@ class MainActivity : AppCompatActivity() {
class GetMediaActivityResultContract : ActivityResultContracts.GetContent() {
@SuppressLint("MissingSuperCall")
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
// Force only images and videos to be selectable
// Force OPEN Document because of the resulting URI must be passed to the
// Playback service and the picker's permissions only allow the activity to read the URI
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
// Force only images and videos to be selectable
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
}
}

View File

@@ -92,24 +92,32 @@ open class NewMediaModel : ViewModel() {
} else {
uploadingPercentage.value = 0.2f
uploadingDescription.value = "Uploading"
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = serverToUse,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
createNIP94Record(imageUrl, mimeType, description, sensitiveContent, relayList = relayList)
},
onError = {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
viewModelScope.launch(Dispatchers.IO) {
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = serverToUse,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
createNIP94Record(
imageUrl,
mimeType,
description,
sensitiveContent,
relayList = relayList
)
},
onError = {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
}
)
)
}
}
},
onError = {

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.util.Log
import android.util.Size
import android.widget.Toast
import androidx.compose.foundation.Image
@@ -79,13 +80,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
var showRelaysDialog by remember {
mutableStateOf(false)
}
var relayList = account.activeRelays()?.filter {
it.write
}?.map {
it
} ?: account.convertLocalRelays().filter {
it.write
}
var relayList = account.activeWriteRelays()
Dialog(
onDismissRequest = { onClose() },
@@ -101,7 +96,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
) {
if (showRelaysDialog) {
RelaySelectionDialog(
list = relayList,
preSelectedList = relayList,
onClose = {
showRelaysDialog = false
},
@@ -221,7 +216,8 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
try {
bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null)
} catch (e: Exception) {
postViewModel.imageUploadingError.emit("Unable to load file")
postViewModel.imageUploadingError.emit("Unable to load thumbnail, but the video can be uploaded")
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
}
}
}

View File

@@ -101,37 +101,50 @@ class NewMessageTagger(
key = key.removePrefix("@")
if (key.length < 63) {
return null
}
try {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
// Converts to npub
val pubkey = Nip19.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val pubkeyRelay = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val noteRelayId = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val address = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}

View File

@@ -9,6 +9,7 @@ import android.util.Size
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -82,20 +83,24 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.CancelIcon
import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -111,6 +116,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Math.round
@Composable
fun NewPostView(
@@ -133,13 +139,7 @@ fun NewPostView(
var showRelaysDialog by remember {
mutableStateOf(false)
}
var relayList = account.activeRelays()?.filter {
it.write
}?.map {
it
} ?: account.convertLocalRelays().filter {
it.write
}
var relayList = account.activeWriteRelays()
LaunchedEffect(Unit) {
postViewModel.load(account, baseReplyTo, quote)
@@ -169,7 +169,7 @@ fun NewPostView(
) {
if (showRelaysDialog) {
RelaySelectionDialog(
list = relayList,
preSelectedList = relayList,
onClose = {
showRelaysDialog = false
},
@@ -327,9 +327,9 @@ fun NewPostView(
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp)
) {
FowardZapTo(postViewModel)
FowardZapTo(postViewModel, accountViewModel)
}
}
@@ -727,7 +727,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
}
@Composable
fun FowardZapTo(postViewModel: NewPostViewModel) {
fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
@@ -761,7 +761,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
}
Text(
text = stringResource(R.string.zap_forward_title),
text = stringResource(R.string.zap_split_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
@@ -771,22 +771,52 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
Divider()
Text(
text = stringResource(R.string.zap_forward_explainer),
text = stringResource(R.string.zap_split_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
postViewModel.forwardZapTo.items.forEachIndexed { index, splitItem ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
BaseUserPicture(splitItem.key, Size55dp, accountViewModel = accountViewModel)
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
UsernameDisplay(splitItem.key, showPlayButton = false)
Text(
text = String.format("%.0f%%", splitItem.percentage * 100),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Spacer(modifier = DoubleHorzSpacer)
Slider(
value = splitItem.percentage,
onValueChange = { sliderValue ->
val rounded = (round(sliderValue * 20)) / 20.0f
postViewModel.updateZapPercentage(index, rounded)
},
modifier = Modifier
.weight(1.5f)
)
}
}
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
label = { Text(text = stringResource(R.string.zap_split_serarch_and_add_user)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
color = MaterialTheme.colors.placeholderText
)
},
@@ -1376,8 +1406,8 @@ fun ImageVideoDescription(
try {
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
} catch (e: Exception) {
onError("Unable to load file")
Log.e("NewPostView", "Couldn't create thumbnail for $uri")
onError("Unable to load thumbnail")
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
}
}
}

View File

@@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.Split
import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AddressableEvent
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -88,9 +90,13 @@ open class NewPostViewModel() : ViewModel() {
var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false)
data class ForwardZapSetup(val user: User) {
var percentage by mutableStateOf(100)
}
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<User?>(null)
var forwardZapTo by mutableStateOf<Split<User>>(Split())
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
// NSFW, Sensitive
@@ -151,7 +157,7 @@ open class NewPostViewModel() : ViewModel() {
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
this.account = account
@@ -166,10 +172,13 @@ open class NewPostViewModel() : ViewModel() {
val dmUsers = toUsersTagger.mentions
val zapReceiver = if (wantsForwardZapTo) {
if (forwardZapTo != null) {
forwardZapTo?.info?.lud16 ?: forwardZapTo?.info?.lud06
} else {
forwardZapToEditting.text
forwardZapTo?.items?.map {
ZapSplitSetup(
lnAddressOrPubKeyHex = it.key.pubkeyHex,
relay = it.key.relaysBeingUsed.keys.firstOrNull(),
weight = it.percentage.toDouble(),
isLnAddress = false
)
}
} else {
null
@@ -355,7 +364,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
forwardZapTo = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
userSuggestions = emptyList()
@@ -427,10 +436,10 @@ open class NewPostViewModel() : ViewModel() {
open fun updateZapForwardTo(it: TextFieldValue) {
forwardZapToEditting = it
if (it.selection.collapsed) {
val lastWord = it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWord = it.text
userSuggestionAnchor = it.selection
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
if (lastWord.startsWith("@") && lastWord.length > 2) {
if (lastWord.length > 2) {
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
viewModelScope.launch(Dispatchers.IO) {
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
@@ -460,6 +469,9 @@ open class NewPostViewModel() : ViewModel() {
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
)
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo?.addItem(item)
forwardZapToEditting = TextFieldValue("")
/*
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkeyNpub()}"
@@ -468,7 +480,7 @@ open class NewPostViewModel() : ViewModel() {
forwardZapToEditting = TextFieldValue(
forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
)
)*/
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord = toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
@@ -656,6 +668,10 @@ open class NewPostViewModel() : ViewModel() {
isValidvalueMaximum.value = true
}
}
fun updateZapPercentage(index: Int, sliderValue: Float) {
forwardZapTo?.updatePercentage(index, sliderValue)
}
}
enum class GeohashPrecision(val digits: Int) {

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayPool
@@ -24,6 +25,7 @@ class NewRelayListViewModel : ViewModel() {
fun load(account: Account) {
this.account = account
clear()
loadRelayDocuments()
}
fun create() {
@@ -36,17 +38,34 @@ class NewRelayListViewModel : ViewModel() {
clear()
}
fun loadRelayDocuments() {
viewModelScope.launch(Dispatchers.IO) {
_relays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo(
dirtyUrl = item.url,
onInfo = {
togglePaidRelay(item, it.limitation?.payment_required ?: false)
},
onError = { url, errorCode, exceptionMessage ->
}
)
}
}
}
fun clear() {
_relays.update {
var relayFile = account.userProfile().latestContactList?.relays()
// Ugly, but forces nostr.band as the only search-supporting relay today.
// TODO: Remove when search becomes more available.
if (relayFile?.none { it.key == Constants.forcedRelayForSearch.url } == true) {
relayFile = relayFile + Pair(
Constants.forcedRelayForSearch.url,
ContactListEvent.ReadWrite(Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write)
)
if (relayFile?.none { it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet } == true) {
relayFile = relayFile + Constants.forcedRelayForSearch.map {
Pair(
it.url,
ContactListEvent.ReadWrite(it.read, it.write)
)
}
}
if (relayFile != null) {

View File

@@ -17,6 +17,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -37,6 +38,7 @@ import kotlinx.coroutines.withContext
fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel()
val context = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
postViewModel.load(account)
@@ -82,7 +84,9 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
}
Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
modifier = Modifier
.padding(10.dp)
.verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier.fillMaxWidth(1f),

View File

@@ -124,7 +124,6 @@ class NewUserMetadataViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
}
clear()
}
@@ -183,23 +182,25 @@ class NewUserMetadataViewModel : ViewModel() {
contentResolver.getType(galleryUri),
context.applicationContext,
onReady = { fileUri, contentType, size ->
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = account.defaultFileServer,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
onUploading(false)
onUploaded(imageUrl)
},
onError = {
onUploading(false)
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
viewModelScope.launch(Dispatchers.IO) {
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = account.defaultFileServer,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
onUploading(false)
onUploaded(imageUrl)
},
onError = {
onUploading(false)
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
}
)
)
}
},
onError = {
onUploading(false)

View File

@@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -28,27 +25,24 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RelayInformationDialog(
onClose: () -> Unit,
relayBriefInfo: RelayBriefInfo,
relayInfo: RelayInformation,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -79,18 +73,25 @@ fun RelayInformationDialog(
})
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Title(relayInfo.name ?: "")
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = StdPadding.fillMaxWidth()) {
Column() {
RenderRelayIcon(
relayBriefInfo.favIcon,
Size55dp
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
SectionContent(relayInfo.description ?: "")
Spacer(modifier = DoubleHorzSpacer)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row() {
Title(relayInfo.name?.trim() ?: "")
}
Row() {
SubtitleContent(relayInfo.description?.trim() ?: "")
}
}
}
Section(stringResource(R.string.owner))
@@ -212,7 +213,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) {
val text = item.toString().padStart(2, '0')
Box(Modifier.padding(10.dp)) {
ClickableUrl(
urlText = "$text",
urlText = text,
url = "https://github.com/nostr-protocol/nips/blob/master/$text.md"
)
}
@@ -222,7 +223,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) {
val text = item.padStart(2, '0')
Box(Modifier.padding(10.dp)) {
ClickableUrl(
urlText = "$text",
urlText = text,
url = "https://github.com/nostr-protocol/nips/blob/master/$text.md"
)
}
@@ -258,13 +259,18 @@ private fun DisplayOwnerInformation(
@Composable
fun Title(text: String) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = text,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
Spacer(modifier = DoubleVertSpacer)
}
@Composable
fun SubtitleContent(text: String) {
Text(
text = text
)
}
@Composable
@@ -285,85 +291,3 @@ fun SectionContent(text: String) {
text = text
)
}
fun loadRelayInfo(
dirtyUrl: String,
context: Context,
scope: CoroutineScope,
onInfo: (RelayInformation) -> Unit
) {
try {
val url = if (dirtyUrl.contains("://")) {
dirtyUrl
.replace("wss://", "https://")
.replace("ws://", "http://")
} else {
"https://$dirtyUrl"
}
val request: Request = Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClient.getHttpClient()
.newCall(request)
.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
checkNotInMainThread()
response.use {
val body = it.body.string()
try {
if (it.isSuccessful) {
onInfo(RelayInformation.fromJson(body))
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl),
Toast.LENGTH_SHORT
).show()
}
}
} catch (e: Exception) {
Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e)
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl),
Toast.LENGTH_SHORT
).show()
}
}
}
}
override fun onFailure(call: Call, e: IOException) {
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl),
Toast.LENGTH_SHORT
).show()
}
}
}
)
} catch (e: Exception) {
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl),
Toast.LENGTH_SHORT
).show()
}
}
}

View File

@@ -29,19 +29,27 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
data class RelayList(
val relay: Relay,
val relayInfo: RelayBriefInfo,
val isSelected: Boolean
)
data class RelayInfoDialog(
val relayBriefInfo: RelayBriefInfo,
val relayInfo: RelayInformation
)
@Composable
fun RelaySelectionDialog(
list: List<Relay>,
preSelectedList: List<Relay>,
onClose: () -> Unit,
onPost: (list: List<Relay>) -> Unit,
accountViewModel: AccountViewModel,
@@ -49,34 +57,29 @@ fun RelaySelectionDialog(
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val relayList = accountViewModel.account.activeRelays()?.filter {
it.write
}?.map {
it
} ?: accountViewModel.account.convertLocalRelays().filter {
it.write
}
var relays by remember {
mutableStateOf(
relayList.map {
accountViewModel.account.activeWriteRelays().map {
RelayList(
it,
list.any { relay -> it.url == relay.url }
relay = it,
relayInfo = RelayBriefInfo(it.url),
isSelected = preSelectedList.any { relay -> it.url == relay.url }
)
}
)
}
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
if (relayInfo != null) {
relayInfo?.let {
RelayInformationDialog(
onClose = {
relayInfo = null
},
relayInfo = relayInfo!!,
accountViewModel,
nav
relayInfo = it.relayInfo,
relayBriefInfo = it.relayBriefInfo,
accountViewModel = accountViewModel,
nav = nav
)
}
@@ -115,14 +118,14 @@ fun RelaySelectionDialog(
}
)
PostButton(
SaveButton(
onPost = {
val selectedRelays = relays.filter { it.isSelected }
if (selectedRelays.isEmpty()) {
scope.launch {
Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show()
}
return@PostButton
return@SaveButton
}
onPost(selectedRelays.map { it.relay })
onClose()
@@ -153,10 +156,7 @@ fun RelaySelectionDialog(
key = { _, item -> item.relay.url }
) { index, item ->
RelaySwitch(
text = item.relay.url
.removePrefix("ws://")
.removePrefix("wss://")
.removeSuffix("/"),
text = item.relayInfo.displayUrl,
checked = item.isSelected,
onClick = {
relays = relays.mapIndexed { j, item ->
@@ -168,9 +168,30 @@ fun RelaySelectionDialog(
}
},
onLongPress = {
loadRelayInfo(item.relay.url, context, scope) {
relayInfo = it
}
accountViewModel.retrieveRelayDocument(
item.relay.url,
onInfo = {
relayInfo = RelayInfoDialog(RelayBriefInfo(item.relay.url), it)
},
onError = { url, errorCode, exceptionMessage ->
val msg = when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
)
.show()
}
}
)
}
)
}

View File

@@ -59,38 +59,38 @@ fun buildAnnotatedStringWithUrlHighlighting(text: AnnotatedString, color: Color)
)
)
newWord + restOfWord
} else if (Patterns.WEB_URL.matcher(word).matches()) {
val startIndex = builderBefore.toString().length
val endIndex = startIndex + word.length
val startNew = builderAfter.toString().length
val endNew = startNew + word.length
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, endNew)
)
)
builderBefore.append("$word ")
builderAfter.append("$word ")
word
} else {
builderBefore.append(word + " ")
builderAfter.append(word + " ")
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
} catch (e: Exception) {
// if it can't parse the key, don't try to change.
builderBefore.append(word + " ")
builderAfter.append(word + " ")
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
}.joinToString(" ")
}.joinToString("\n")
)
val newText = toAnnotatedString()
newText.split("\\s+".toRegex()).filter { word ->
Patterns.WEB_URL.matcher(word).matches()
}.forEach {
val startIndex = text.indexOf(it)
val endIndex = startIndex + it.length
addStyle(
style = SpanStyle(
color = color,
textDecoration = TextDecoration.None
),
start = startIndex,
end = endIndex
)
}
substitutions.forEach {
addStyle(
style = SpanStyle(

View File

@@ -0,0 +1,99 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlin.math.abs
class SplitItem<T>(val key: T) {
var percentage by mutableStateOf(0f)
}
class Split<T>() {
var items: List<SplitItem<T>> by mutableStateOf(emptyList())
fun addItem(key: T): Int {
val wasEqualSplit = isEqualSplit()
val newItem = SplitItem(key)
items = items.plus(newItem)
if (wasEqualSplit) {
forceEqualSplit()
} else {
updatePercentage(items.lastIndex, equalSplit())
}
return items.lastIndex
}
fun equalSplit() = 1f / items.size
fun isEqualSplit(): Boolean {
val expectedPercentage = equalSplit()
return items.all { (it.percentage - expectedPercentage) < 0.01 }
}
fun forceEqualSplit() {
val correctPercentage = equalSplit()
items.forEach {
it.percentage = correctPercentage
}
}
fun updatePercentage(index: Int, percentage: Float) {
if (items.isEmpty()) return
val splitItem = items.getOrNull(index) ?: return
if (items.size == 1) {
splitItem.percentage = 1f
} else {
splitItem.percentage = percentage
println("Update ${items[index].key} to $percentage")
val othersMustShare = 1.0f - splitItem.percentage
val othersHave = items.sumOf {
if (it == splitItem) 0.0 else it.percentage.toDouble()
}.toFloat()
if (abs(othersHave - othersMustShare) < 0.01) return // nothing to do
println("Others Must Share $othersMustShare but have $othersHave")
bottomUpAdjustment(othersMustShare, othersHave, index)
}
}
private fun bottomUpAdjustment(othersMustShare: Float, othersHave: Float, exceptForIndex: Int) {
var needToRemove = othersHave - othersMustShare
if (needToRemove > 0) {
for (i in items.indices.reversed()) {
if (i == exceptForIndex) continue // do not update the current item
if (needToRemove < items[i].percentage) {
val oldValue = items[i].percentage
items[i].percentage -= needToRemove
needToRemove = 0f
println("- Updating ${items[i].key} from $oldValue to ${items[i].percentage - needToRemove}. $needToRemove left")
} else {
val oldValue = items[i].percentage
needToRemove -= items[i].percentage
items[i].percentage = 0f
println("- Updating ${items[i].key} from $oldValue to ${0}. $needToRemove left")
}
if (needToRemove < 0.01) {
break
}
}
} else if (needToRemove < 0) {
if (items.lastIndex == exceptForIndex) {
items[items.lastIndex - 1].percentage += -needToRemove
} else {
items.last().percentage += -needToRemove
}
}
}
}

View File

@@ -704,6 +704,7 @@ fun ControlWhenPlayerIsActive(
controller.addListener(listener)
onDispose {
view.keepScreenOn = false
controller.removeListener(listener)
}
}

View File

@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.quartz.encoders.toHexKey
object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
lateinit var account: Account
@@ -12,18 +14,44 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
}
override fun feed(): List<Note> {
val privKey = account.keyPair.privKey ?: return emptyList()
val bookmarks = account.userProfile().latestBookmarkList
val notes = bookmarks?.privateTaggedEvents(privKey)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
if (account.loginWithExternalSigner) {
val id = bookmarks?.id
if (id != null) {
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
if (decryptedContent == null) {
ExternalSignerUtils.decryptBookmark(
bookmarks.content,
account.keyPair.pubKey.toHexKey(),
id
)
} else {
bookmarks.decryptedContent = decryptedContent
}
}
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: ""
val addresses = bookmarks?.privateTaggedAddresses(privKey)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
val notes = bookmarks?.privateTaggedEvents(decryptedContent)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
val addresses = bookmarks?.privateTaggedAddresses(decryptedContent)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
} else {
val privKey = account.keyPair.privKey ?: return emptyList()
val notes = bookmarks?.privateTaggedEvents(privKey)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
val addresses = bookmarks?.privateTaggedAddresses(privKey)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
}
}
}

View File

@@ -14,7 +14,18 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
}
override fun feed(): List<User> {
return account.getBlockList()
val blockList = account.getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (account.loginWithExternalSigner) {
if (decryptedContent.isEmpty()) return emptyList()
return blockList
?.publicAndPrivateUsers(decryptedContent)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
}
return blockList
?.publicAndPrivateUsers(account.keyPair.privKey)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()

View File

@@ -1,22 +1,30 @@
package com.vitorpamplona.amethyst.ui.dal
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class ThreadFeedFilter(val noteId: String) : FeedFilter<Note>() {
class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter<Note>() {
override fun feedKey(): String {
return noteId
}
override fun feed(): List<Note> {
val cachedSignatures: MutableMap<Note, String> = mutableMapOf()
val eventsToWatch = ThreadAssembler().findThreadFor(noteId) ?: emptySet()
val cachedSignatures: MutableMap<Note, Note.LevelSignature> = mutableMapOf()
val followingSet = account.selectedUsersFollowList(KIND3_FOLLOWS) ?: emptySet()
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
val now = TimeUtils.now()
// Currently orders by date of each event, descending, at each level of the reply stack
val order = compareByDescending<Note> { it.replyLevelSignature(eventsToWatch, cachedSignatures) }
val order = compareByDescending<Note> {
it.replyLevelSignature(eventsInHex, cachedSignatures, account.userProfile(), followingSet, now).signature
}
return eventsToWatch.sortedWith(order)
}

View File

@@ -451,8 +451,6 @@ fun GenericMainTopBar(
nav: (String) -> Unit,
content: @Composable (AccountViewModel) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = BottomTopHeight) {
MyTopAppBar(
elevation = 0.dp,
@@ -476,6 +474,7 @@ fun GenericMainTopBar(
}
},
navigationIcon = {
val coroutineScope = rememberCoroutineScope()
LoggedInUserPictureDrawer(accountViewModel) {
coroutineScope.launch {
scaffoldState.drawerState.open()
@@ -506,10 +505,9 @@ private fun LoggedInUserPictureDrawer(
accountViewModel: AccountViewModel,
onClick: () -> Unit
) {
val accountUserState by accountViewModel.account.userProfile().live().metadata.observeAsState()
val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
val pubkeyHex = remember { accountUserState?.user?.pubkeyHex ?: "" }
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() }
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
IconButton(
onClick = onClick

View File

@@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -63,7 +64,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
@@ -72,11 +72,12 @@ import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
@@ -109,7 +110,7 @@ fun DrawerContent(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 100.dp),
.padding(top = 70.dp),
scaffoldState,
accountViewModel,
nav
@@ -162,7 +163,7 @@ fun ProfileContent(
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.height(120.dp)
)
} else {
Image(
@@ -171,7 +172,7 @@ fun ProfileContent(
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.height(120.dp)
)
}
@@ -220,7 +221,6 @@ fun ProfileContent(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 15.dp)
.clickable(
onClick = {
nav(route)
@@ -455,7 +455,6 @@ fun ListContent(
}
val coroutineScope = rememberCoroutineScope()
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
var wantsToEditRelays by remember {
mutableStateOf(false)
}
@@ -491,7 +490,7 @@ fun ListContent(
)
IconRowRelays(
relayViewModel = relayViewModel,
accountViewModel = accountViewModel,
onClick = {
coroutineScope.launch {
scaffoldState.drawerState.close()
@@ -634,27 +633,37 @@ private fun enableTor(
}
@Composable
private fun RelayStatus(
relayViewModel: RelayPoolViewModel
) {
val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--")
val isConnected by relayViewModel.isConnected.observeAsState(false)
private fun RelayStatus(accountViewModel: AccountViewModel) {
val connectedRelaysText by RelayPool.statusFlow.collectAsState(initial = RelayPoolStatus(0, 0))
RenderRelayStatus(connectedRelaysText, isConnected)
RenderRelayStatus(connectedRelaysText)
}
@Composable
private fun RenderRelayStatus(
connectedRelaysText: String,
isConnected: Boolean
relayPool: RelayPoolStatus
) {
val text by remember(relayPool) {
derivedStateOf {
"${relayPool.connected}/${relayPool.available}"
}
}
val placeHolder = MaterialTheme.colors.placeholderText
val color by remember(relayPool) {
derivedStateOf {
if (relayPool.isConnected) {
placeHolder
} else {
Color.Red
}
}
}
Text(
text = connectedRelaysText,
color = if (isConnected) {
MaterialTheme.colors.placeholderText
} else {
Color.Red
},
text = text,
color = color,
style = MaterialTheme.typography.subtitle1
)
}
@@ -710,7 +719,7 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl
}
@Composable
fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -737,7 +746,7 @@ fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
Spacer(modifier = Modifier.width(Size16dp))
RelayStatus(relayViewModel = relayViewModel)
RelayStatus(accountViewModel = accountViewModel)
}
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -36,6 +37,8 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
@@ -80,6 +83,7 @@ class AddBountyAmountViewModel : ViewModel() {
fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onClose: () -> Unit) {
val postViewModel: AddBountyAmountViewModel = viewModel()
postViewModel.load(accountViewModel.account, bounty)
val scope = rememberCoroutineScope()
Dialog(
onDismissRequest = { onClose() },
@@ -89,7 +93,11 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
)
) {
Surface() {
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
Column(
modifier = Modifier
.padding(10.dp)
.width(IntrinsicSize.Min)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -102,8 +110,10 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
PostButton(
onPost = {
postViewModel.sendPost()
onClose()
scope.launch(Dispatchers.IO) {
postViewModel.sendPost()
onClose()
}
},
isActive = postViewModel.hasChanged()
)

View File

@@ -62,57 +62,63 @@ fun BlankNote(modifier: Modifier = Modifier, showDivider: Boolean = false, idHex
@Composable
fun HiddenNote(
reports: ImmutableSet<Note>,
isHiddenAuthor: Boolean,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
isQuote: Boolean = false,
nav: (String) -> Unit,
onClick: () -> Unit
) {
Column(modifier = modifier) {
Row(modifier = Modifier.padding(horizontal = if (!isQuote) 12.dp else 6.dp)) {
Column(modifier = Modifier.padding(start = if (!isQuote) 10.dp else 5.dp)) {
Row(
modifier = Modifier.padding(
start = 20.dp,
end = 20.dp
),
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(30.dp)) {
Text(
text = stringResource(R.string.post_was_flagged_as_inappropriate_by),
color = Color.Gray
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Row(
modifier = Modifier.padding(start = if (!isQuote) 30.dp else 25.dp, end = 20.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(30.dp)
) {
Text(
text = stringResource(R.string.post_was_flagged_as_inappropriate_by),
color = Color.Gray
)
FlowRow(modifier = Modifier.padding(top = 10.dp)) {
if (isHiddenAuthor) {
UserPicture(
user = accountViewModel.userProfile(),
size = Size35dp,
nav = nav,
accountViewModel = accountViewModel
)
}
reports.forEach {
NoteAuthorPicture(
baseNote = it,
size = Size35dp,
nav = nav,
accountViewModel = accountViewModel
)
FlowRow(modifier = Modifier.padding(top = 10.dp)) {
reports.forEach {
NoteAuthorPicture(
baseNote = it,
nav = nav,
accountViewModel = accountViewModel,
size = Size35dp
)
}
}
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.show_anyway), color = Color.White)
}
}
}
Divider(
thickness = 0.25.dp
)
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.show_anyway), color = Color.White)
}
}
}
Divider(
thickness = 0.25.dp
)
}
}

View File

@@ -76,7 +76,6 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -135,13 +134,9 @@ fun CheckHiddenChannelCardCompose(
nav: (String) -> Unit
) {
if (showHidden) {
var state by remember {
val state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
@@ -185,19 +180,14 @@ fun LoadedChannelCardCompose(
) {
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) {
state = newState
}
@@ -219,7 +209,7 @@ fun LoadedChannelCardCompose(
@Composable
fun RenderChannelCardReportState(
state: NoteComposeReportState,
state: AccountViewModel.NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
@@ -233,6 +223,7 @@ fun RenderChannelCardReportState(
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
false,

View File

@@ -256,11 +256,10 @@ private fun UserRoomCompose(
note.createdAt()
}
}
val content by remember(note) {
derivedStateOf {
mutableStateOf(
accountViewModel.decrypt(note)
}
)
}
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->

View File

@@ -69,8 +69,6 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -152,17 +150,13 @@ fun LoadedChatMessageCompose(
) {
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
WatchForReports(baseNote, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
WatchForReports(baseNote, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
@@ -178,6 +172,7 @@ fun LoadedChatMessageCompose(
if (it) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
Modifier,
innerQuote,
@@ -632,7 +627,7 @@ private fun RenderRegularTextNote(
nav: (String) -> Unit
) {
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
val eventContent = remember { accountViewModel.decrypt(note) }
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) {
@@ -641,7 +636,7 @@ private fun RenderRegularTextNote(
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
content = eventContent,
content = eventContent!!,
canPreview = canPreview,
modifier = modifier,
tags = tags,

View File

@@ -1,7 +1,10 @@
package com.vitorpamplona.amethyst.ui.note
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -358,6 +361,12 @@ private fun ParseAuthorCommentAndAmount(
onReady(content)
}
fun click(content: MutableState<ZapAmountCommentNotification>, nav: (String) -> Unit) {
content.value.user?.let {
nav(routeFor(it))
}
}
@Composable
private fun RenderState(
content: MutableState<ZapAmountCommentNotification>,
@@ -366,11 +375,7 @@ private fun RenderState(
nav: (String) -> Unit
) {
Row(
modifier = Modifier.clickable {
content.value.user?.let {
nav(routeFor(it))
}
},
modifier = Modifier.clickable { click(content, nav) },
verticalAlignment = Alignment.CenterVertically
) {
DisplayAuthorCommentAndAmount(
@@ -418,8 +423,19 @@ fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotifi
@Composable
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
Crossfade(authorComment.value, modifier = amountBoxModifier) {
it.amount?.let {
val visible by remember(authorComment) {
derivedStateOf {
authorComment.value.amount != null
}
}
AnimatedVisibility(
visible = visible,
modifier = amountBoxModifier,
enter = fadeIn(),
exit = fadeOut()
) {
authorComment.value.amount?.let {
Box(
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
@@ -453,8 +469,18 @@ fun CrossfadeToDisplayComment(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Crossfade(authorComment.value) {
it.comment?.let {
val visible by remember(authorComment) {
derivedStateOf {
authorComment.value.comment != null
}
}
AnimatedVisibility(
visible,
enter = fadeIn(),
exit = fadeOut()
) {
authorComment.value.comment?.let {
TranslatableRichTextViewer(
content = it,
canPreview = true,

View File

@@ -31,14 +31,17 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
@@ -83,6 +86,7 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
@@ -117,11 +121,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
@@ -173,6 +179,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -196,7 +203,6 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
@@ -277,11 +283,7 @@ fun CheckHiddenNoteCompose(
// Ignores reports as well
val state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
@@ -324,13 +326,6 @@ fun CheckHiddenNoteCompose(
}
}
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean,
val canPreview: Boolean,
val relevantReports: ImmutableSet<Note>
)
@Composable
fun LoadedNoteCompose(
note: Note,
@@ -347,19 +342,14 @@ fun LoadedNoteCompose(
) {
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) {
state = newState
}
@@ -386,7 +376,7 @@ fun LoadedNoteCompose(
@Composable
fun RenderReportState(
state: NoteComposeReportState,
state: AccountViewModel.NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
@@ -405,6 +395,7 @@ fun RenderReportState(
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
isBoostedNote,
@@ -436,7 +427,7 @@ fun RenderReportState(
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (Boolean, Boolean, ImmutableSet<Note>) -> Unit
onChange: (AccountViewModel.NoteComposeReportState) -> Unit
) {
val userFollowsState by accountViewModel.userFollows.observeAsState()
val noteReportsState by note.live().reports.observeAsState()
@@ -1071,6 +1062,62 @@ private fun NoteBody(
accountViewModel,
nav
)
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = HalfDoubleVertSpacer)
DisplayZapSplits(noteEvent, accountViewModel, nav)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
if (list.isEmpty()) return
Row(verticalAlignment = CenterVertically) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Spacer(modifier = StdHorzSpacer)
FlowRow {
list.forEach {
if (it.isLnAddress) {
ClickableText(
text = AnnotatedString(it.lnAddressOrPubKeyHex),
onClick = { },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
UserPicture(userHex = it.lnAddressOrPubKeyHex, size = 25.dp, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
}
@Composable
@@ -1576,10 +1623,8 @@ private fun RenderPrivateMessage(
val noteEvent = note.event as? PrivateDmEvent ?: return
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
if (withMe) {
val eventContent = remember { accountViewModel.decrypt(note) }
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
@@ -1589,7 +1634,7 @@ private fun RenderPrivateMessage(
if (eventContent != null) {
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
text = eventContent!!,
color = MaterialTheme.colors.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
@@ -1600,7 +1645,7 @@ private fun RenderPrivateMessage(
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
content = eventContent,
content = eventContent!!,
canPreview = canPreview && !makeItShort,
modifier = modifier,
tags = tags,
@@ -1610,7 +1655,7 @@ private fun RenderPrivateMessage(
)
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
DisplayUncitedHashtags(hashtags, eventContent!!, nav)
}
}
} else {
@@ -1641,9 +1686,9 @@ fun DisplayRelaySet(
) {
val noteEvent = baseNote.event as? RelaySetEvent ?: return
val relays by remember {
mutableStateOf<ImmutableList<String>>(
noteEvent.relays().toImmutableList()
val relays by remember(baseNote) {
mutableStateOf(
noteEvent.relays().map { RelayBriefInfo(it) }.toImmutableList()
)
}
@@ -1698,7 +1743,7 @@ fun DisplayRelaySet(
toMembersShow.forEach { relay ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
Text(
relay.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/"),
text = relay.displayUrl,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -1708,7 +1753,7 @@ fun DisplayRelaySet(
)
Column(modifier = Modifier.padding(start = 10.dp)) {
RelayOptionsAction(relay, accountViewModel, nav)
RelayOptionsAction(relay.url, accountViewModel, nav)
}
}
}
@@ -2414,40 +2459,41 @@ private fun ReplyRow(
}
}
val showChannelInfo by remember(note) {
derivedStateOf {
if (noteEvent is ChannelMessageEvent || noteEvent is LiveActivitiesChatMessageEvent) {
note.channelHex()
} else {
null
}
}
}
showChannelInfo?.let {
ChannelHeader(
channelHex = it,
showVideo = false,
showBottomDiviser = false,
sendToChannel = true,
modifier = MaterialTheme.colors.replyModifier.padding(10.dp),
accountViewModel = accountViewModel,
nav = nav
)
}
if (showReply) {
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } }
if (replyingDirectlyTo != null && unPackReply) {
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
Spacer(modifier = StdVertSpacer)
} else {
// ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
}
} else {
val showChannelReply by remember {
derivedStateOf {
(noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) ||
(noteEvent is LiveActivitiesChatMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()))
} else if (showChannelInfo != null) {
val replies = remember { note.replyTo?.toImmutableList() }
val mentions = remember {
(note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList()
?: persistentListOf()
}
}
if (showChannelReply) {
val channelHex = note.channelHex()
channelHex?.let {
ChannelHeader(
channelHex = channelHex,
showVideo = false,
showBottomDiviser = false,
sendToChannel = true,
modifier = remember { Modifier.padding(vertical = 5.dp) },
accountViewModel = accountViewModel,
nav = nav
)
val replies = remember { note.replyTo?.toImmutableList() }
val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() }
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
}
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
}
}
}
@@ -2680,14 +2726,14 @@ private fun BadgeBox(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isRepost by remember {
val isRepost by remember(baseNote) {
derivedStateOf {
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
}
}
if (isRepost) {
val baseReply by remember {
val baseReply by remember(baseNote) {
derivedStateOf {
baseNote.replyTo?.lastOrNull()
}

View File

@@ -24,6 +24,7 @@ import androidx.compose.ui.window.Popup
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -35,6 +36,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@@ -283,6 +286,12 @@ fun ZapVote(
}
var wantsToZap by remember { mutableStateOf(false) }
var wantsToPay by remember {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
persistentListOf()
)
}
var zappingProgress by remember { mutableStateOf(0f) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
@@ -298,7 +307,7 @@ fun ZapVote(
// interactionSource = remember { MutableInteractionSource() },
// indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
if (!accountViewModel.isWriteable()) {
if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
@@ -362,6 +371,8 @@ fun ZapVote(
zappingProgress = it
}
},
onPayViaIntent = {
},
zapType = accountViewModel.account.defaultZapType
)
} else {
@@ -393,10 +404,19 @@ fun ZapVote(
scope.launch(Dispatchers.Main) {
zappingProgress = it
}
},
onPayViaIntent = {
wantsToPay = it
}
)
}
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
wantsToPay = persistentListOf()
}
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
@@ -463,7 +483,8 @@ fun FilteredZapAmountChoicePopup(
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onProgress: (percent: Float) -> Unit
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
val context = LocalContext.current
@@ -502,6 +523,7 @@ fun FilteredZapAmountChoicePopup(
context,
onError,
onProgress,
onPayViaIntent,
defaultZapType
)
onDismiss()
@@ -526,6 +548,7 @@ fun FilteredZapAmountChoicePopup(
context,
onError,
onProgress,
onPayViaIntent,
defaultZapType
)
onDismiss()

View File

@@ -78,6 +78,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
@@ -107,6 +108,8 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
@@ -543,12 +546,16 @@ fun ReplyReaction(
if (accountViewModel.isWriteable()) {
onPress()
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
Toast.LENGTH_SHORT
).show()
if (accountViewModel.loggedInWithExternalSigner()) {
onPress()
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
Toast.LENGTH_SHORT
).show()
}
}
}
}
@@ -652,12 +659,22 @@ fun BoostReaction(
wantsToBoost = true
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
Toast.LENGTH_SHORT
).show()
if (accountViewModel.loggedInWithExternalSigner()) {
if (accountViewModel.hasBoosted(baseNote)) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteBoostsTo(baseNote)
}
} else {
wantsToBoost = true
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
Toast.LENGTH_SHORT
).show()
}
}
}
}
@@ -675,6 +692,11 @@ fun BoostReaction(
onQuote = {
wantsToBoost = false
onQuotePress()
},
onRepost = {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
}
}
)
}
@@ -738,6 +760,20 @@ fun LikeReaction(
context,
onMultipleChoices = {
wantsToReact = true
},
onWantsToSignReaction = {
if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
} else if (accountViewModel.account.reactionChoices.size > 1) {
wantsToReact = true
}
}
)
},
@@ -857,7 +893,8 @@ private fun likeClick(
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onMultipleChoices: () -> Unit
onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit
) {
if (accountViewModel.account.reactionChoices.isEmpty()) {
scope.launch {
@@ -870,14 +907,18 @@ private fun likeClick(
.show()
}
} else if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_like_posts),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
onWantsToSignReaction()
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_like_posts),
Toast.LENGTH_SHORT
)
.show()
}
}
} else if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
@@ -907,6 +948,11 @@ fun ZapReaction(
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var wantsToPay by remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
persistentListOf()
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -932,6 +978,15 @@ fun ZapReaction(
},
onMultipleChoices = {
wantsToZap = true
},
onError = {
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
},
onPayViaIntent = {
wantsToPay = it
}
)
},
@@ -965,6 +1020,9 @@ fun ZapReaction(
scope.launch(Dispatchers.Main) {
zappingProgress = it
}
},
onPayViaIntent = {
wantsToPay = it
}
)
}
@@ -975,7 +1033,10 @@ fun ZapReaction(
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null }
@@ -983,11 +1044,38 @@ fun ZapReaction(
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
UpdateZapAmountDialog(
onClose = { wantsToChangeZapAmount = false },
accountViewModel = accountViewModel
)
}
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
wantsToPay = persistentListOf()
}
}
if (wantsToSetCustomZap) {
ZapCustomDialog({ wantsToSetCustomZap = false }, accountViewModel, baseNote)
ZapCustomDialog(
onClose = { wantsToSetCustomZap = false },
onError = {
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
},
onProgress = {
scope.launch(Dispatchers.Main) {
zappingProgress = it
}
},
onPayViaIntent = {
wantsToPay = it
},
accountViewModel = accountViewModel,
baseNote = baseNote
)
}
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
@@ -1020,7 +1108,9 @@ private fun zapClick(
scope: CoroutineScope,
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit
onMultipleChoices: () -> Unit,
onError: (String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
scope.launch {
@@ -1032,7 +1122,7 @@ private fun zapClick(
)
.show()
}
} else if (!accountViewModel.isWriteable()) {
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
@@ -1049,20 +1139,14 @@ private fun zapClick(
null,
"",
context,
onError = {
scope.launch {
onZappingProgress(0f)
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
},
onError = onError,
onProgress = {
scope.launch(Dispatchers.Main) {
onZappingProgress(it)
}
},
zapType = accountViewModel.account.defaultZapType
zapType = accountViewModel.account.defaultZapType,
onPayViaIntent = onPayViaIntent
)
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
onMultipleChoices()
@@ -1178,7 +1262,7 @@ private fun DrawViewCount(
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) {
private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit, onRepost: () -> Unit) {
val iconSizePx = with(LocalDensity.current) {
-iconSize.toPx().toInt()
}
@@ -1193,8 +1277,13 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
if (accountViewModel.isWriteable()) {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
onDismiss()
}
} else {
onRepost()
onDismiss()
}
},
@@ -1373,10 +1462,11 @@ fun ZapAmountChoicePopup(
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onProgress: (percent: Float) -> Unit
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapMessage = ""
@@ -1399,6 +1489,7 @@ fun ZapAmountChoicePopup(
context,
onError,
onProgress,
onPayViaIntent,
account.defaultZapType
)
onDismiss()
@@ -1423,6 +1514,7 @@ fun ZapAmountChoicePopup(
context,
onError,
onProgress,
onPayViaIntent,
account.defaultZapType
)
onDismiss()

View File

@@ -11,17 +11,16 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
@@ -29,80 +28,40 @@ import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifie
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
public fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
var showShowMore by remember { mutableStateOf(false) }
var lazyRelayList by remember {
val baseNumber = baseNote.relays.map {
it.removePrefix("wss://").removePrefix("ws://")
}.toImmutableList()
mutableStateOf(baseNumber)
}
var shortRelayList by remember {
mutableStateOf(lazyRelayList.take(3).toImmutableList())
}
val scope = rememberCoroutineScope()
WatchRelayLists(baseNote) { relayList ->
if (!equalImmutableLists(relayList, lazyRelayList)) {
scope.launch(Dispatchers.Main) {
lazyRelayList = relayList
shortRelayList = relayList.take(3).toImmutableList()
}
}
val nextShowMore = relayList.size > 3
if (nextShowMore != showShowMore) {
scope.launch(Dispatchers.Main) {
// only triggers recomposition when actually different
showShowMore = nextShowMore
}
val relayList by baseNote.live().relayInfo.observeAsState(persistentListOf())
val shortRelayList by remember {
derivedStateOf {
relayList.take(3).toImmutableList()
}
}
Spacer(DoubleVertSpacer)
if (expanded) {
VerticalRelayPanelWithFlow(lazyRelayList, accountViewModel, nav)
VerticalRelayPanelWithFlow(relayList, accountViewModel, nav)
} else {
VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav)
}
if (showShowMore && !expanded) {
if (relayList.size > 3 && !expanded) {
ShowMoreRelaysButton {
expanded = true
}
}
}
@Composable
private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList<String>) -> Unit) {
val noteRelaysState by baseNote.live().relays.observeAsState()
LaunchedEffect(key1 = noteRelaysState) {
launch(Dispatchers.IO) {
val relayList = noteRelaysState?.note?.relays?.map {
it.removePrefix("wss://").removePrefix("ws://")
} ?: emptyList()
onListChanges(relayList.toImmutableList())
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
relays: ImmutableList<String>,
relays: ImmutableList<RelayBriefInfo>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -17,7 +18,6 @@ import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -29,13 +29,15 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
@@ -44,6 +46,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@@ -59,15 +63,13 @@ public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewMo
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RenderRelayList(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteRelays by baseNote.live().relays.map {
it.note.relays
}.observeAsState(baseNote.relays)
val noteRelays by baseNote.live().relayInfo.observeAsState()
FlowRow(StdStartPadding) {
val relaysToDisplay = remember(noteRelays, expanded.value) {
if (expanded.value) noteRelays else noteRelays.take(3)
if (expanded.value) noteRelays else noteRelays?.take(3)?.toImmutableList()
}
relaysToDisplay.forEach {
relaysToDisplay?.forEach {
RenderRelay(it, accountViewModel, nav)
}
}
@@ -104,14 +106,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) {
}
@Composable
fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val iconUrl by remember(dirtyUrl) {
derivedStateOf {
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
"https://$cleanUrl/favicon.ico"
}
}
fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
if (relayInfo != null) {
@@ -120,8 +115,9 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri
relayInfo = null
},
relayInfo = relayInfo!!,
accountViewModel,
nav
relayBriefInfo = relay,
accountViewModel = accountViewModel,
nav = nav
)
}
@@ -130,7 +126,7 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp)
val clickableModifier = remember(dirtyUrl) {
val clickableModifier = remember(relay) {
Modifier
.padding(1.dp)
.size(Size15dp)
@@ -139,9 +135,30 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri
interactionSource = interactionSource,
indication = ripple,
onClick = {
loadRelayInfo(dirtyUrl, context, scope) {
relayInfo = it
}
accountViewModel.retrieveRelayDocument(
relay.url,
onInfo = {
relayInfo = it
},
onError = { url, errorCode, exceptionMessage ->
val msg = when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
)
.show()
}
}
)
}
)
}
@@ -149,17 +166,17 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri
Box(
modifier = clickableModifier
) {
RenderRelayIcon(iconUrl)
RenderRelayIcon(relay.favIcon)
}
}
@Composable
private fun RenderRelayIcon(iconUrl: String) {
fun RenderRelayIcon(iconUrl: String, size: Dp = Size13dp) {
val backgroundColor = MaterialTheme.colors.background
val iconModifier = remember {
Modifier
.size(Size13dp)
.size(size)
.clip(shape = CircleShape)
.background(backgroundColor)
}

View File

@@ -197,7 +197,11 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
fun UpdateZapAmountDialog(
onClose: () -> Unit,
nip47uri: String? = null,
accountViewModel: AccountViewModel
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()

View File

@@ -43,11 +43,13 @@ import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -73,9 +75,7 @@ fun NoteAuthorPicture(
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
val author by baseNote.live().metadata.map {
it.note.author
}.distinctUntilChanged().observeAsState(baseNote.author)
val author by baseNote.live().authorChanges.observeAsState(baseNote.author)
Crossfade(targetState = author) {
if (it == null) {
@@ -104,6 +104,32 @@ fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
)
}
@Composable
fun UserPicture(
userHex: String,
size: Dp,
pictureModifier: Modifier = remember { Modifier },
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadUser(baseUserHex = userHex) {
if (it != null) {
UserPicture(
user = it,
size = size,
pictureModifier = pictureModifier,
accountViewModel = accountViewModel,
nav = nav
)
} else {
DisplayBlankAuthor(
size,
pictureModifier
)
}
}
}
@Composable
fun UserPicture(
user: User,
@@ -112,23 +138,19 @@ fun UserPicture(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val route by remember {
val route by remember(user) {
derivedStateOf {
"User/${user.pubkeyHex}"
}
}
val scope = rememberCoroutineScope()
ClickableUserPicture(
baseUser = user,
size = size,
accountViewModel = accountViewModel,
modifier = pictureModifier,
onClick = {
scope.launch {
nav(route)
}
nav(route)
}
)
}
@@ -274,9 +296,7 @@ fun InnerBaseUserPicture(
accountViewModel: AccountViewModel,
modifier: Modifier
) {
val userProfile by baseUser.live().metadata.map {
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
val userProfile by baseUser.live().profilePictureChanges.observeAsState(baseUser.profilePicture())
PictureAndFollowingMark(
userHex = baseUser.pubkeyHex,
@@ -391,11 +411,15 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
val scope = rememberCoroutineScope()
if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = {
accountViewModel.follow(
note.author ?: return@DropdownMenuItem
); onDismiss()
}) {
DropdownMenuItem(
onClick = {
val author = note.author ?: return@DropdownMenuItem
scope.launch(Dispatchers.IO) {
accountViewModel.follow(author)
onDismiss()
}
}
) {
Text(stringResource(R.string.follow))
}
Divider()
@@ -446,36 +470,104 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
Text(stringResource(R.string.quick_action_share))
}
Divider()
if (state.isPrivateBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_private_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_private_bookmarks))
}
}
if (state.isPublicBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_public_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_public_bookmarks))
}
}
Divider()
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
Text(stringResource(R.string.request_deletion))
if (state.isPrivateBookmarkNote) {
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithExternalSigner()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
ExternalSignerUtils.decrypt(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey(),
bookmarks?.id ?: ""
)
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
accountViewModel.removePrivateBookmark(note, bookmarks?.decryptedContent ?: "")
} else {
accountViewModel.removePrivateBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.remove_from_private_bookmarks))
}
} else {
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text(stringResource(R.string.block_report))
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithExternalSigner()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
ExternalSignerUtils.decrypt(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey(),
bookmarks?.id ?: ""
)
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
accountViewModel.addPrivateBookmark(note, bookmarks?.decryptedContent ?: "")
} else {
accountViewModel.addPrivateBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.add_to_private_bookmarks))
}
}
if (state.isPublicBookmarkNote) {
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithExternalSigner()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
ExternalSignerUtils.decrypt(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey(),
bookmarks?.id ?: ""
)
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
accountViewModel.removePublicBookmark(
note,
bookmarks?.decryptedContent ?: ""
)
} else {
accountViewModel.removePublicBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.remove_from_public_bookmarks))
}
} else {
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithExternalSigner()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
ExternalSignerUtils.decrypt(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey(),
bookmarks?.id ?: ""
)
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
accountViewModel.addPublicBookmark(
note,
bookmarks?.decryptedContent ?: ""
)
} else {
accountViewModel.addPublicBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.add_to_public_bookmarks))
}
}
Divider()
@@ -494,6 +586,16 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
Text(stringResource(R.string.content_warning_see_warnings))
}
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
Text(stringResource(R.string.request_deletion))
}
} else {
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text(stringResource(R.string.block_report))
}
}
}
if (reportDialogShowing) {
@@ -508,9 +610,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
val followState by accountViewModel.userProfile().live().follows.observeAsState()
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val showSensitiveContent by accountViewModel.accountLiveData.map {
it.account.showSensitiveContent
}.distinctUntilChanged().observeAsState(accountViewModel.account.showSensitiveContent)
val showSensitiveContent by accountViewModel.showSensitiveContentChanges.observeAsState(accountViewModel.account.showSensitiveContent)
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
launch(Dispatchers.IO) {

View File

@@ -1,38 +1,62 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ZapOptionstViewModel : ViewModel() {
private var account: Account? = null
@@ -61,17 +85,21 @@ class ZapOptionstViewModel : ViewModel() {
}
@Composable
fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, baseNote: Note) {
fun ZapCustomDialog(
onClose: () -> Unit,
onError: (text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
accountViewModel: AccountViewModel,
baseNote: Note
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val postViewModel: ZapOptionstViewModel = viewModel()
LaunchedEffect(accountViewModel) {
postViewModel.load(accountViewModel.account)
}
var zappingProgress by remember { mutableStateOf(0f) }
val zapTypes = listOf(
Triple(LnZapEvent.ZapType.PUBLIC, stringResource(id = R.string.zap_type_public), stringResource(id = R.string.zap_type_public_explainer)),
Triple(LnZapEvent.ZapType.PRIVATE, stringResource(id = R.string.zap_type_private), stringResource(id = R.string.zap_type_private_explainer)),
@@ -111,20 +139,11 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
null,
postViewModel.customMessage.text,
context,
onError = {
zappingProgress = 0f
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
},
onProgress = {
scope.launch(Dispatchers.Main) {
zappingProgress = it
}
},
onError = onError,
onProgress = onProgress,
onPayViaIntent = onPayViaIntent,
zapType = selectedZapType
)
onClose()
}
}
@@ -275,3 +294,115 @@ fun ErrorMessageDialog(
}
)
}
@Composable
fun PayViaIntentDialog(
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
accountViewModel: AccountViewModel,
onClose: () -> Unit
) {
val context = LocalContext.current
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
Surface() {
Column(modifier = Modifier.padding(10.dp)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onPress = onClose)
}
Spacer(modifier = DoubleVertSpacer)
payingInvoices.forEachIndexed { index, it ->
val paid = remember {
mutableStateOf(false)
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
if (it.user != null) {
BaseUserPicture(it.user, Size55dp, accountViewModel = accountViewModel)
} else {
DisplayBlankAuthor(size = Size55dp)
}
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
if (it.user != null) {
UsernameDisplay(it.user, showPlayButton = false)
} else {
Text(
text = stringResource(id = R.string.wallet_number, index + 1),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Row() {
Text(
text = showAmount((it.amountMilliSats / 1000.0f).toBigDecimal()),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Spacer(modifier = StdHorzSpacer)
Text(
text = stringResource(id = R.string.sats),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
}
Spacer(modifier = DoubleHorzSpacer)
PayButton(isActive = !paid.value) {
paid.value = true
val uri = "lightning:" + it.invoice
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
}
}
}
}
}
}
}
@Composable
fun PayButton(isActive: Boolean, modifier: Modifier = Modifier, onPost: () -> Unit = {}) {
Button(
modifier = modifier,
onClick = {
onPost()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
),
contentPadding = PaddingValues(0.dp)
) {
if (isActive) {
Text(text = stringResource(R.string.pay), color = Color.White)
} else {
Text(text = stringResource(R.string.paid), color = Color.White)
}
}
}

View File

@@ -173,6 +173,8 @@ fun UserActionOptions(
baseAuthor: User,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
WatchIsHiddenUser(baseAuthor, accountViewModel) { isHidden ->
if (isHidden) {
ShowUserButton {
@@ -209,14 +211,20 @@ fun ShowFollowingOrUnfollowingButton(
if (isFollowing) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.unfollow(baseAuthor)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -227,14 +235,20 @@ fun ShowFollowingOrUnfollowingButton(
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseAuthor)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {

View File

@@ -45,21 +45,21 @@ class AccountStateViewModel(val context: Context) : ViewModel() {
}
}
fun startUI(key: String, useProxy: Boolean, proxyPort: Int) {
fun startUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray()
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val account =
if (key.startsWith("nsec")) {
Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort)
Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
} else if (pubKeyParsed != null) {
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort)
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
} else if (EMAIL_PATTERN.matcher(key).matches()) {
// Evaluate NIP-5
Account(KeyPair(), proxy = proxy, proxyPort = proxyPort)
Account(KeyPair(), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
} else {
Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort)
Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
}
LocalPreferences.updatePrefsForLogin(account)

View File

@@ -95,10 +95,10 @@ class NostrDiscoverChatFeedViewModel(val account: Account) : FeedViewModel(Disco
}
}
class NostrThreadFeedViewModel(val noteId: String) : FeedViewModel(ThreadFeedFilter(noteId)) {
class Factory(val noteId: String) : ViewModelProvider.Factory {
class NostrThreadFeedViewModel(account: Account, noteId: String) : FeedViewModel(ThreadFeedFilter(account, noteId)) {
class Factory(val account: Account, val noteId: String) : ViewModelProvider.Factory {
override fun <NostrThreadFeedViewModel : ViewModel> create(modelClass: Class<NostrThreadFeedViewModel>): NostrThreadFeedViewModel {
return NostrThreadFeedViewModel(noteId) as NostrThreadFeedViewModel
return NostrThreadFeedViewModel(account, noteId) as NostrThreadFeedViewModel
}
}
}

View File

@@ -1,20 +0,0 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.relays.RelayPool
@Stable
class RelayPoolViewModel : ViewModel() {
val connectionStatus = RelayPool.live.map {
val connectedRelays = it.relays.connectedRelays()
val availableRelays = it.relays.availableRelays()
"$connectedRelays/$availableRelays"
}.distinctUntilChanged()
val isConnected = RelayPool.live.map {
it.relays.connectedRelays() > 0
}.distinctUntilChanged()
}

View File

@@ -262,6 +262,7 @@ fun NoteMaster(
HiddenNote(
reports,
note.author?.let { account.isHidden(it) } ?: false,
accountViewModel,
Modifier,
false,

View File

@@ -1,29 +1,29 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountState
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
@@ -33,16 +33,15 @@ import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
@@ -56,22 +55,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
val discoveryListLiveData = accountLiveData.map {
val discoveryListLiveData = account.live.map {
it.account.defaultDiscoveryFollowList
}.distinctUntilChanged()
val homeListLiveData = accountLiveData.map {
val homeListLiveData = account.live.map {
it.account.defaultHomeFollowList
}.distinctUntilChanged()
val notificationListLiveData = accountLiveData.map {
val notificationListLiveData = account.live.map {
it.account.defaultNotificationFollowList
}.distinctUntilChanged()
val storiesListLiveData = accountLiveData.map {
val storiesListLiveData = account.live.map {
it.account.defaultStoriesFollowList
}.distinctUntilChanged()
val showSensitiveContentChanges = account.live.map {
it.account.showSensitiveContent
}.distinctUntilChanged()
fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: ConnectivityType
) {
@@ -94,6 +97,10 @@ class AccountViewModel(val account: Account) : ViewModel() {
return account.isWriteable()
}
fun loggedInWithExternalSigner(): Boolean {
return account.loginWithExternalSigner
}
fun userProfile(): User {
return account.userProfile()
}
@@ -205,78 +212,22 @@ class AccountViewModel(val account: Account) : ViewModel() {
return null
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
fun zap(
note: Note,
amount: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
zapType: LnZapEvent.ZapType
) {
viewModelScope.launch(Dispatchers.IO) {
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
ZapPaymentHandler(account).zap(note, amount, pollOption, message, context, onError, onProgress, onPayViaIntent, zapType)
}
}
private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) {
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
return
}
var zapRequestJson = ""
if (zapType != LnZapEvent.ZapType.NONZAP) {
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType)
if (zapRequest != null) {
zapRequestJson = zapRequest.toJson()
}
}
onProgress(0.10f)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount,
message,
zapRequestJson,
onSuccess = {
onProgress(0.7f)
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(
bolt11 = it,
note,
onResponse = { response ->
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
onError(
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
)
} else {
onProgress(1f)
}
}
)
onProgress(0.8f)
// Awaits for the event to come back to LocalCache.
viewModelScope.launch(Dispatchers.IO) {
delay(5000)
onProgress(0f)
}
} else {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found))
}
onProgress(0f)
}
},
onError = onError,
onProgress = onProgress
)
}
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
account.report(note, type, content)
}
@@ -304,10 +255,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
account.addPrivateBookmark(note)
}
fun addPrivateBookmark(note: Note, decryptedContent: String) {
account.addPrivateBookmark(note, decryptedContent)
}
fun addPublicBookmark(note: Note, decryptedContent: String) {
account.addPublicBookmark(note, decryptedContent)
}
fun removePublicBookmark(note: Note, decryptedContent: String) {
account.removePublicBookmark(note, decryptedContent)
}
fun addPublicBookmark(note: Note) {
account.addPublicBookmark(note)
}
fun removePrivateBookmark(note: Note, decryptedContent: String) {
account.removePrivateBookmark(note, decryptedContent)
}
fun removePrivateBookmark(note: Note) {
account.removePrivateBookmark(note)
}
@@ -410,14 +377,24 @@ class AccountViewModel(val account: Account) : ViewModel() {
return account.defaultZapType
}
fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet<Note>) -> Unit) {
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean = true,
val canPreview: Boolean = true,
val isHiddenAuthor: Boolean = false,
val relevantReports: ImmutableSet<Note> = persistentSetOf()
)
fun isNoteAcceptable(note: Note, onReady: (NoteComposeReportState) -> Unit) {
viewModelScope.launch {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onReady(true, true, persistentSetOf())
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else if (note.author?.let { account.isHidden(it) } == true) {
onReady(NoteComposeReportState(false, false, true, persistentSetOf()))
} else {
val newCanPreview = !note.hasAnyReports()
@@ -425,11 +402,18 @@ class AccountViewModel(val account: Account) : ViewModel() {
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onReady(true, true, persistentSetOf())
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else {
val newRelevantReports = account.getRelevantReports(note)
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
onReady(
NoteComposeReportState(
newIsAcceptable,
newCanPreview,
false,
newRelevantReports.toImmutableSet()
)
)
}
}
}
@@ -529,6 +513,16 @@ class AccountViewModel(val account: Account) : ViewModel() {
}
}
fun retrieveRelayDocument(
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
Nip11CachedRetriever.loadRelayInfo(dirtyUrl, onInfo, onError)
}
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel

View File

@@ -171,14 +171,20 @@ fun GeoHashActionOptions(
if (isFollowingTag) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowGeohash(tag)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -189,14 +195,20 @@ fun GeoHashActionOptions(
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followGeohash(tag)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {

View File

@@ -147,14 +147,20 @@ fun HashtagActionOptions(
if (isFollowingTag) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowHashtag(tag)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -165,14 +171,20 @@ fun HashtagActionOptions(
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followHashtag(tag)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {

View File

@@ -9,15 +9,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.*
import androidx.compose.material.DrawerValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Scaffold
import androidx.compose.material.rememberDrawerState
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
@@ -28,6 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -37,12 +29,6 @@ import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
import com.vitorpamplona.amethyst.ui.navigation.*
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
import com.vitorpamplona.amethyst.ui.navigation.AppTopBar
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
import com.vitorpamplona.amethyst.ui.screen.AccountState
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
@@ -251,11 +237,14 @@ fun FloatingButtons(
navScrollToTop: (Route, Boolean) -> Unit
) {
val accountState by accountStateViewModel.accountContent.collectAsState()
val context = LocalContext.current
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is AccountState.LoggedInViewOnly -> {
// Does nothing.
if (accountViewModel.loggedInWithExternalSigner()) {
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
}
}
is AccountState.LoggedOff -> {
// Does nothing.

View File

@@ -621,7 +621,9 @@ private fun ProfileHeader(
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
onClick = { popupExpanded = true },
onClick = {
popupExpanded = true
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
@@ -714,6 +716,8 @@ private fun ProfileActions(
EditButton(accountViewModel.account)
}
val scope = rememberCoroutineScope()
WatchIsHiddenUser(baseUser, accountViewModel) { isHidden ->
if (isHidden) {
ShowUserButton {
@@ -744,14 +748,20 @@ private fun DisplayFollowUnfollowButton(
if (isLoggedInFollowingUser) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(baseUser)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -763,14 +773,20 @@ private fun DisplayFollowUnfollowButton(
if (isUserFollowingLoggedIn) {
FollowButton(R.string.follow_back) {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -781,14 +797,20 @@ private fun DisplayFollowUnfollowButton(
} else {
FollowButton(R.string.follow) {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
@@ -1688,17 +1710,21 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
if (accountViewModel.userProfile() != user) {
Divider()
if (accountViewModel.account.isHidden(user)) {
DropdownMenuItem(onClick = {
accountViewModel.show(user)
onDismiss()
}) {
DropdownMenuItem(
onClick = {
accountViewModel.show(user)
onDismiss()
}
) {
Text(stringResource(R.string.unblock_user))
}
} else {
DropdownMenuItem(onClick = {
accountViewModel.hide(user)
onDismiss()
}) {
DropdownMenuItem(
onClick = {
accountViewModel.hide(user)
onDismiss()
}
) {
Text(stringResource(id = R.string.block_hide_user))
}
}

View File

@@ -23,7 +23,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
val feedViewModel: NostrThreadFeedViewModel = viewModel(
key = noteId + "NostrThreadFeedViewModel",
factory = NostrThreadFeedViewModel.Factory(noteId)
factory = NostrThreadFeedViewModel.Factory(accountViewModel.account, noteId)
)
NostrThreadDataSource.loadThread(noteId)

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteComposeReportState
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.RenderRelay
@@ -76,7 +76,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -128,7 +127,6 @@ fun WatchAccountForVideoScreen(videoFeedView: NostrVideoFeedViewModel, accountVi
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SaveableFeedState(
videoFeedView: NostrVideoFeedViewModel,
@@ -250,19 +248,14 @@ fun LoadedVideoCompose(
) {
var state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) {
state = newState
}
@@ -281,7 +274,7 @@ fun LoadedVideoCompose(
@Composable
fun RenderReportState(
state: NoteComposeReportState,
state: AccountViewModel.NoteComposeReportState,
note: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -290,11 +283,12 @@ fun RenderReportState(
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
if (showHiddenNote) {
Column(remember { Modifier.fillMaxSize(1f) }, verticalArrangement = Arrangement.Center) {
Column(remember { Modifier.fillMaxSize() }, verticalArrangement = Arrangement.Center) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
Modifier,
Modifier.fillMaxWidth(),
false,
nav,
onClick = { showReportedNote = true }
@@ -421,14 +415,11 @@ private fun VideoUserOptionAction(
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteRelaysState by baseNote.live().relays.observeAsState()
val noteRelays = remember(noteRelaysState) {
noteRelaysState?.note?.relays ?: emptySet()
}
val noteRelays by baseNote.live().relayInfo.observeAsState()
FlowRow() {
noteRelays.forEach { dirtyUrl ->
RenderRelay(dirtyUrl, accountViewModel, nav)
noteRelays?.forEach { relayInfo ->
RenderRelay(relayInfo, accountViewModel, nav)
}
}
}

View File

@@ -1,7 +1,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedOff
import android.content.Context
import android.app.Activity
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -36,13 +39,20 @@ import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.PackageUtils
import com.vitorpamplona.amethyst.service.SignerType
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@OptIn(ExperimentalComposeUiApi::class)
@@ -63,6 +73,58 @@ fun LoginPage(
val useProxy = remember { mutableStateOf(false) }
val proxyPort = remember { mutableStateOf("9050") }
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var loginWithExternalSigner by remember { mutableStateOf(false) }
val activity = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = {
loginWithExternalSigner = false
ExternalSignerUtils.isActivityRunning = false
ServiceManager.shouldPauseService = true
if (it.resultCode != Activity.RESULT_OK) {
scope.launch(Dispatchers.Main) {
Toast.makeText(
Amethyst.instance,
"Sign request rejected",
Toast.LENGTH_SHORT
).show()
}
return@rememberLauncherForActivityResult
} else {
val event = it.data?.getStringExtra("signature") ?: ""
key.value = TextFieldValue(event)
if (!acceptedTerms.value) {
termsAcceptanceIsRequired =
context.getString(R.string.acceptance_of_terms_is_required)
}
if (key.value.text.isBlank()) {
errorMessage = context.getString(R.string.key_is_required)
}
if (acceptedTerms.value && key.value.text.isNotBlank()) {
try {
accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt(), true)
} catch (e: Exception) {
Log.e("Login", "Could not sign in", e)
errorMessage = context.getString(R.string.invalid_key)
}
}
}
}
)
LaunchedEffect(loginWithExternalSigner) {
if (loginWithExternalSigner) {
ExternalSignerUtils.openSigner(
"",
SignerType.GET_PUBLIC_KEY,
activity,
"",
""
)
}
}
Column(
modifier = Modifier
@@ -229,7 +291,7 @@ fun LoginPage(
}
}
if (isPackageInstalled(context, "org.torproject.android")) {
if (PackageUtils.isOrbotInstalled(context)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = useProxy.value,
@@ -290,6 +352,53 @@ fun LoginPage(
Text(text = stringResource(R.string.login))
}
}
if (PackageUtils.isAmberInstalled(context)) {
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
Button(
onClick = {
val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login"), "")
if (result == null) {
loginWithExternalSigner = true
return@Button
}
key.value = TextFieldValue(result)
if (!acceptedTerms.value) {
termsAcceptanceIsRequired =
context.getString(R.string.acceptance_of_terms_is_required)
}
if (key.value.text.isBlank()) {
errorMessage = context.getString(R.string.key_is_required)
}
if (acceptedTerms.value && key.value.text.isNotBlank()) {
try {
accountViewModel.startUI(
key.value.text,
useProxy.value,
proxyPort.value.toInt(),
true
)
} catch (e: Exception) {
Log.e("Login", "Could not sign in", e)
errorMessage = context.getString(R.string.invalid_key)
}
}
},
shape = RoundedCornerShape(Size35dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
)
) {
Text(text = stringResource(R.string.login_with_external_signer))
}
}
}
}
// The last child is glued to the bottom.
@@ -315,7 +424,3 @@ fun LoginPage(
)
}
}
fun isPackageInstalled(context: Context, target: String): Boolean {
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
}

View File

@@ -24,3 +24,9 @@ val DarkerGreen = Color.Green.copy(alpha = 0.32f)
val WarningColor = Color(0xFFC62828)
val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
val LightWarningColor = Color(0xFFffcc00)
val DarkWarningColor = Color(0xFFF8DE22)
val LightAllGoodColor = Color(0xFF339900)
val DarkAllGoodColor = Color(0xFF99cc33)

View File

@@ -286,6 +286,12 @@ val Colors.overPictureBackground: Color
val Colors.bitcoinColor: Color
get() = if (isLight) BitcoinLight else BitcoinDark
val Colors.warningColor: Color
get() = if (isLight) LightWarningColor else DarkWarningColor
val Colors.allGoodColor: Color
get() = if (isLight) LightAllGoodColor else DarkAllGoodColor
val Colors.markdownStyle: RichTextStyle
get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="point_to_the_qr_code">أشر إلى رمز QR</string>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">أشر إلى رمز (QR Code)</string>
<string name="show_qr">أظهر QR</string>
<string name="profile_image">صورة الملف الشخصي</string>
<string name="scan_qr">مسح QR</string>
<string name="scan_qr">مسح رمز ال QR</string>
<string name="show_anyway">عرض على أي حال</string>
<string name="post_was_flagged_as_inappropriate_by">تم وضع علامة على المنشور على أنه غير لائق بواسطة</string>
<string name="post_not_found">المنشور غير موجود</string>
@@ -12,17 +12,19 @@
<string name="could_not_decrypt_the_message">لا يمكن فك تشفير الرسالة</string>
<string name="group_picture">صورة المجموعة</string>
<string name="explicit_content">محتوى فاضح</string>
<string name="spam">رسائل عشوائية</string>
<string name="spam">محتوى عشوائي</string>
<string name="impersonation">التمثيل</string>
<string name="illegal_behavior">تصرف غير قانوني</string>
<string name="unknown">غير معروف</string>
<string name="relay_icon">Relay Icon</string>
<string name="relay_icon">أيقونة الخادم</string>
<string name="unknown_author">كاتب غير معروف</string>
<string name="copy_text">نسخ النص</string>
<string name="copy_user_pubkey">نسخ المفتاح العام</string>
<string name="copy_note_id">نسخ معرف </string>
<string name="broadcast">بث</string>
<string name="block_hide_user">حظر و إخفاء المستخدم</string>
<string name="request_deletion">حذف الطلب</string>
<string name="block_report">حظر / تبليغ</string>
<string name="block_hide_user"><![CDATA[حظر و إخفاء المستخدم]]></string>
<string name="report_spam_scam">الابلاغ عن البريد العشوائي / الاحتيال</string>
<string name="report_impersonation">الإبلاغ عن التمثيل</string>
<string name="report_explicit_content">الإبلاغ عن المحتوى الصريح / الفاضح</string>
@@ -32,19 +34,22 @@
<string name="login_with_a_private_key_to_like_posts">تسجيل الدخول باستخدام مفتاح خاص لإبداء الإعجاب بالمنشورات</string>
<string name="no_zap_amount_setup_long_press_to_change">لا يوجد إعداد لمبلغ Zap. اضغط لفترة طويلة للتغيير</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">قم بتسجيل الدخول باستخدام مفتاح خاص لتتمكن من إرسال Zaps</string>
<string name="zaps">Zaps</string>
<string name="login_with_a_private_key_to_be_able_to_follow">قم بتسجيل الدخول باستخدام مفتاح خاص لتتمكن من متابعة المستخدمين</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">قم بتسجيل الدخول بمفتاحك الخاص للتتمكن من إلغاء متابعة المستخدمين </string>
<string name="view_count">مشاهدة العد</string>
<string name="boost">تعزيز</string>
<string name="boosted">معزز</string>
<string name="quote">إقتباس</string>
<string name="new_amount_in_sats">مبلغ جديد في Sats</string>
<string name="add">إضافة</string>
<string name="replying_to">الرد على</string>
<string name="replying_to">"الرد على"</string>
<string name="and">" و "</string>
<string name="in_channel">"في القناة "</string>
<string name="profile_banner">لافتة الملف الشخصي</string>
<string name="following">المتابعون</string>
<string name="followers">متابِعاً</string>
<string name="payment_successful">تم الدفع بنجاح</string>
<string name="error_parsing_error_message">خطأ أثناء تحليل رسالة الخطأ</string>
<string name="following">"المتابعون"</string>
<string name="followers">"متابِعاً"</string>
<string name="profile">الملف الشخصي</string>
<string name="security_filters">مرشحات الأمان</string>
<string name="log_out">تسجيل الخروج</string>
@@ -54,24 +59,25 @@
<string name="lightning_tips">Lightning تبرع</string>
<string name="note_to_receiver">ملاحظة للمستقبل</string>
<string name="thank_you_so_much">شكرا جزيلا!</string>
<string name="amount_in_sats">المبلغ في Sats</string>
<string name="amount_in_sats">المبلغ ب الSats</string>
<string name="send_sats">إرسال Sats</string>
<string name="error_parsing_preview_for">خطأ في تحليل المعاينة لـ %1$s : %2$s</string>
<string name="preview_card_image_for">معاينة صورة البطاقة لـ %1$s</string>
<string name="error_parsing_preview_for">"خطأ في تحليل المعاينة لـ %1$s : %2$s"</string>
<string name="preview_card_image_for">"معاينة صورة البطاقة لـ %1$s"</string>
<string name="new_channel">قناة جديدة</string>
<string name="channel_name">اسم القناة</string>
<string name="my_awesome_group">مجموعتي الرائعة</string>
<string name="picture_url">رابط الصورة</string>
<string name="description">الوصف</string>
<string name="about_us">معلومات عن القناة..</string>
<string name="about_us">"معلومات عن القناة.."</string>
<string name="what_s_on_your_mind">ما الذي يدور في ذهنك؟</string>
<string name="post">نشر</string>
<string name="save">حفظ</string>
<string name="create">إنشاء</string>
<string name="cancel">إلغاء</string>
<string name="failed_to_upload_the_image">فشل تحميل الصورة</string>
<string name="relay_address">Relay عنوان</string>
<string name="relay_address"> عنوان الخادم (Relay Adress)</string>
<string name="posts">المنشورات</string>
<string name="bytes">Bytes</string>
<string name="errors">الأخطاء</string>
<string name="home_feed">المنشورات الرئيسية</string>
<string name="private_message_feed"> الرسائل الخاصة</string>
@@ -92,7 +98,7 @@
<string name="failed_to_save_the_image">فشل حفظ الصورة</string>
<string name="upload_image">تحميل الصورة</string>
<string name="uploading">جاري التحميل…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">لا يمتلك المستخدم إعداد عنوان lighting لاستقبال sats</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">لا يمتلك المستخدم عنوان (Lightning Address) لاستقبال sats</string>
<string name="reply_here">"الرد هنا.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">نسخ معرف الملاحظة إلى الحافظة للمشاركة</string>
<string name="copy_channel_id_note_to_the_clipboard">انسخ معرف القناة (ملاحظة) إلى الحافظة</string>
@@ -105,10 +111,10 @@
<string name="conversations">المحادثات</string>
<string name="notes">الملاحظات</string>
<string name="replies">الردود</string>
<string name="follows">المتابَعون</string>
<string name="reports">التقارير</string>
<string name="follows">"المتابَعون"</string>
<string name="reports">"التقارير"</string>
<string name="more_options">المزيد من الخيارات</string>
<string name="relays">Relays</string>
<string name="relays">"Relays"</string>
<string name="website">الموقع</string>
<string name="lightning_address">Lightning عنوان</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">نسخ معرف Nsec (كلمة المرور الخاصة بك) إلى الحافظة للنسخ الاحتياطي</string>
@@ -121,7 +127,7 @@
<string name="unblock">رفع الحظر</string>
<string name="copy_user_id">نسخ معرف المستخدم</string>
<string name="unblock_user">إلغاء حظر عن مستخدم</string>
<string name="npub_hex_username">npub, hex, username </string>
<string name="npub_hex_username">"npub, hex, username "</string>
<string name="clear">نظف</string>
<string name="app_logo">شعار التطبيق</string>
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string>
@@ -135,7 +141,7 @@
<string name="login">تسجيل دخول</string>
<string name="generate_a_new_key">إنشاء مفتاح جديد</string>
<string name="loading_feed">بإنتظار التحديثات</string>
<string name="error_loading_replies">خطأ في تحميل الردود:</string>
<string name="error_loading_replies">"خطأ في تحميل الردود:"</string>
<string name="try_again">المحاولة مرة اخرى</string>
<string name="feed_is_empty">لا توجد ملاحظات.</string>
<string name="refresh">تحديث</string>
@@ -148,7 +154,7 @@
<string name="leave">مغادرة</string>
<string name="unfollow">الغاء المتابعة</string>
<string name="channel_created">تم انشاء القناة</string>
<string name="channel_information_changed_to">تم تغيير معلومات القناة إلى</string>
<string name="channel_information_changed_to">"تم تغيير معلومات القناة إلى"</string>
<string name="public_chat">الدردشة العامة</string>
<string name="posts_received">تم استلام المشاركات</string>
<string name="remove">ازالة</string>
@@ -168,4 +174,190 @@
<string name="report_hateful_speech">أبلغ عن كلام يحض على الكراهية</string>
<string name="report_nudity_porn">الإبلاغ عن عُري / إباحي</string>
<string name="others">آخرون</string>
<string name="mark_all_known_as_read">ضع علامة على جميع الرسائل/الاشعارات المعروفة كمقروء</string>
<string name="mark_all_new_as_read">ضع علامة على جميع الرسائل/الاشعارات الجديدة كمقروء</string>
<string name="mark_all_as_read">ضع علامة على جميع الرسائل/الاشعارات كمقروء</string>
<string name="backup_keys">قم بعمل نسخة احتياطية للمفاتيح</string>
<string name="account_backup_tips_md" tools:ignore="Typos"> ## النصائح الرئيسية للنسخ الاحتياطي والسلامة
\n\n- يتم تأمين حسابك بمفتاح سري. المفتاح السري هو سلسلة عشوائية طويلة تبدأ بـ **nsec1**. أي شخص لديه حق الوصول إلى مفتاحك السري يمكنه نشر المحتوى باستخدام هويتك.
\n\n- لا تقم بوضع مفتاحك السري في أي موقع أو برنامج لا تثق به.
\n- مطوري Amethyst لن يطلبوا منك **أبدا** المفتاح السري الخاص بك.
\n- حافظ على نسخة احتياطية آمنة لمفتاحك السري لاسترداد الحساب. نوصي باستخدام برنامج لادارة كلمات المرور.
</string>
<string name="secret_key_copied_to_clipboard">مفتاحك السري (nsec) قد تم نسخه في الحافظة</string>
<string name="copy_my_secret_key">إنسخ مفتاحي السري</string>
<string name="biometric_authentication_failed">فشلت المصادقة</string>
<string name="biometric_error">خطأ</string>
<string name="badge_created_by">"تم إنشاؤه بواسطة %1$s"</string>
<string name="badge_award_image_for">"صورة شارة مكافئة ل %1$s"</string>
<string name="new_badge_award_notif">لقد حصلت على شارة جائزة جديدة</string>
<string name="copied_note_text_to_clipboard">تم نسخ نص الملاحظة/المنشور إلى الحافظة</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">تم نسخ عنوان @npub المؤلف إلى الحافظة</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">تم نسخ رمز الملاحظة/المنشور (note ID) إلى الحافظة</string>
<string name="select_text_dialog_top">تحديد نص</string>
<string name="private_conversation_notification">"&lt;استحالة فك تشفير الرسالة الخاصة&gt;
\n\n أنت الآن في محادثة خاصة/مشفرة بين %1$s و %2$s ."</string>
<string name="account_switch_add_account_dialog_title">إضافة حساب جديد</string>
<string name="drawer_accounts">الحسابات</string>
<string name="account_switch_select_account">اختر حساب</string>
<string name="account_switch_add_account_btn">إضافة حساب جديد</string>
<string name="account_switch_active_account">تنشيط الحساب</string>
<string name="account_switch_has_private_key">يحتوي على مفتاح خاص</string>
<string name="account_switch_pubkey_only">قراءة فقط، لا يوجد مفتاح خاص</string>
<string name="back">رجوع</string>
<string name="quick_action_select">اختر</string>
<string name="quick_action_share_browser_link">شارك رابط المتصفح</string>
<string name="quick_action_share">مشاركة</string>
<string name="quick_action_copy_user_id">معرف المؤلف (ID)</string>
<string name="quick_action_copy_note_id">رمز/معرف الملاحظة (Note ID)</string>
<string name="quick_action_copy_text">نسخ النص</string>
<string name="quick_action_delete">حذف</string>
<string name="quick_action_unfollow">الغاء المتابعة</string>
<string name="quick_action_follow">متابعة</string>
<string name="quick_action_request_deletion_alert_title">طلب الحذف</string>
<string name="quick_action_request_deletion_alert_body">سيطلب Amethyst حذف ملاحظتك من الخوادم (relays) الذي أنت متصل به حاليا. لا يوجد ضمان لحذف ملاحظتك بشكل دائم من تلك الخوادم أو من الخوادم الأخرى التي ربما حفظت فيها.</string>
<string name="quick_action_block_dialog_btn">حظر </string>
<string name="quick_action_delete_dialog_btn">حذف</string>
<string name="quick_action_block">حظر</string>
<string name="quick_action_report">التبليغ</string>
<string name="quick_action_delete_button">حذف</string>
<string name="quick_action_dont_show_again_button">لا تظهر مرة أخرى</string>
<string name="report_dialog_illegal">تصرف غير قانوني</string>
<string name="report_dialog_blocking_a_user">حظر المستخدم سوف يخفي محتواه في التطبيق الخاص بك. ملاحظاتك لا تزال قابلة للعرض علناً، بما في ذلك للأشخاص الذين تقوم بحظرهم. يتم إدراج المستخدمين المحظورين على شاشة مرشحات الأمان (Security Filters screen).</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[حظر و إخفاء المستخدم]]></string>
<string name="report_dialog_reminder_public">ستكون جميع التقارير المنشورة مرئية للجمهور.</string>
<string name="report_dialog_additional_reason_placeholder">اختياريا تستطيع توفير سياق إضافي حول تقريرك…</string>
<string name="report_dialog_additional_reason_label">سياق إضافي</string>
<string name="report_dialog_select_reason_label">السبب</string>
<string name="report_dialog_select_reason_placeholder">حدد سببا</string>
<string name="report_dialog_post_report_btn">تبليغ المنشور (Report)</string>
<string name="report_dialog_title">حظر / تبليغ</string>
<string name="block_only">حظر </string>
<string name="bookmarks">المفضلة</string>
<string name="private_bookmarks">المفضلة الخاصة</string>
<string name="public_bookmarks">المفضلة العامة</string>
<string name="add_to_private_bookmarks">إضافة إلى المفضلات الخاصة</string>
<string name="add_to_public_bookmarks">إضافة إلى المفضلات العامة</string>
<string name="remove_from_private_bookmarks">إزالة من المفضلات الخاصة</string>
<string name="remove_from_public_bookmarks">إزالة من المفضلات العامة</string>
<string name="wallet_connect_service">خدمة Wallet Connect </string>
<string name="wallet_connect_service_relay">خادم Wallet Connect </string>
<string name="wallet_connect_service_secret">كلمة سر خدمة Wallet Connect</string>
<string name="wallet_connect_service_show_secret">إظهار كلمة السر</string>
<string name="wallet_connect_service_secret_placeholder">المفتاح السري بصيغة nsec أو hex</string>
<string name="post_poll">نشر الاستطلاع</string>
<string name="poll_heading_required">الحقول المطلوبة:</string>
<string name="poll_primary_description">وصف الاستطلاع الأساسي…</string>
<string name="poll_option_index">الخيار %s</string>
<string name="poll_option_description">وصف خيار الاستطلاع</string>
<string name="poll_heading_optional">حقل اختياري</string>
<string name="poll_zap_value_min">الحد الأدنى</string>
<string name="poll_zap_value_max">الحد الاقصى</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">إغلاق بعد</string>
<string name="poll_closing_time_days">أيام</string>
<string name="poll_is_closed">تم إغلاق الاستطلاع لأي تصويت جديد</string>
<string name="one_vote_per_user_on_atomic_votes">يسمح بصوت واحد فقط لكل مستخدم في هذا النوع من الاستطلاع</string>
<string name="looking_for_event">"البحث عن الحدث %1$s"</string>
<string name="custom_zaps_add_a_message">أضف رسالة عامة</string>
<string name="custom_zaps_add_a_message_private">أضف رسالة خاصة</string>
<string name="custom_zaps_add_a_message_nonzap">أضف رسالة مع الفاتورة</string>
<string name="custom_zaps_add_a_message_example">شكرا لك على كل مساهمتك!</string>
<string name="lightning_create_and_add_invoice">إنشاء وإضافة</string>
<string name="poll_author_no_vote">لا يستطيع مؤلف الاستطلاع التصويت في استطلاعه.</string>
<string name="hash_verification_passed">هذا المحتوى هو نفسه منذ النشر</string>
<string name="hash_verification_failed">لقد تغير هذا المحتوى. ربما لم يشاهد مؤلفه التغيير أو يوافق عليه بعد.</string>
<string name="content_description_add_image">أضف صورة</string>
<string name="content_description_add_video">اضف فيديو</string>
<string name="content_description_add_document">أضف مستند</string>
<string name="add_content">أضف إلى الرسالة</string>
<string name="content_description">وصف المحتوى/المحتويات</string>
<string name="content_description_example">A blue boat in a white sandy beach at sunset
(قارب أزرق في شاطئ رملي أبيض عند غروب الشمس)</string>
<string name="zap_type_public">عام</string>
<string name="zap_type_public_explainer">يستطيع الجميع رؤية المعاملة والرسالة</string>
<string name="zap_type_private">خاص</string>
<string name="zap_type_private_explainer">يمكن للمرسل والمستقبل رؤية بعضهما البعض وقراءة الرسالة</string>
<string name="zap_type_anonymous">مجهول الهوية</string>
<string name="zap_type_anonymous_explainer">المستقبل والجمهور لا يعرفان من الذي قام بإرسال الدفع</string>
<string name="file_server">خادم الملفات</string>
<string name="upload_server_imgur">imgur.com - موثوق به</string>
<string name="upload_server_imgur_explainer">يمكن لـ Imgur تعديل الملف</string>
<string name="upload_server_nostrimg">nostrimg.com - (موثوق به)</string>
<string name="upload_server_nostrimg_explainer">يمكن لـ NostrImg تعديل الملف</string>
<string name="upload_server_nostrbuild">nostr.build- (موثوق به)</string>
<string name="upload_server_nostrbuild_explainer">يمكن لـ Nostr.build تعديل الملف</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - (موثوق به)</string>
<string name="upload_server_nostrfilesdev_explainer">يمكن لـ Nostrfiles.dev تعديل الملف</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - (موثوق به)</string>
<string name="upload_server_nostrcheckme_explainer">يمكن لـ nostrcheck.me تعديل الملف</string>
<string name="upload_server_imgur_nip94_explainer">تحقق مما إذا قام Imgur بتعديل الملف.
NIP جديد: قد لا يراه العملاء الآخرون</string>
<string name="app_notification_zaps_channel_message_from">من طرف %1$s</string>
<string name="app_notification_zaps_channel_message_for">ل %1$s</string>
<string name="reply_notify">تنبيه: </string>
<string name="channel_list_join_conversation">الانضمام إلى المحادثة</string>
<string name="channel_list_user_or_group_id">معرف المستخدم أو المجموعة</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent أو hex</string>
<string name="channel_list_create_channel">إنشاء</string>
<string name="channel_list_join_channel">إنضمام</string>
<string name="today">اليوم</string>
<string name="content_warning">تحذير من المحتوى</string>
<string name="content_warning_hide_all_sensitive_content">إخفاء المحتوى الحساس دائماً</string>
<string name="content_warning_show_all_sensitive_content">إظهار المحتوى الحساس دائماً</string>
<string name="content_warning_see_warnings">إظهار تحذيرات المحتوى دائمًا</string>
<string name="recommended_apps">التطبيقات الموصى بها : </string>
<string name="filter_spam_from_strangers">تصفية الرسائل المزعجة من الغرباء</string>
<string name="warn_when_posts_have_reports_from_your_follows">تنبيه عندما تكون منشورتك قد تم التبليغ عنها </string>
<string name="new_reaction_symbol">رمز رد فعل جديد</string>
<string name="no_reaction_type_setup_long_press_to_change">لم يتم تحديد أي نوع من ردود الفعل. اضغط مطولاً للتغيير</string>
<string name="read_from_relay">القراءة من الخادم (Relay)</string>
<string name="write_to_relay">الكتابة للخادم</string>
<string name="an_error_occurred_trying_to_get_relay_information">حدث خطأ أثناء محاولة الحصول على معلومات الخادم من %1$s</string>
<string name="owner">المالك</string>
<string name="version">الاصدار</string>
<string name="software">البرنامج</string>
<string name="contact">جهة الاتصال</string>
<string name="supports">الNIPs المدعومة</string>
<string name="admission_fees">رسوم القبول</string>
<string name="payments_url">رابط الدفع</string>
<string name="limitations">الحدود</string>
<string name="countries">الدول</string>
<string name="languages">اللغات</string>
<string name="tags">الوسوم</string>
<string name="posting_policy">سياسات النشر</string>
<string name="message_length">طول الرسالة</string>
<string name="subscriptions">الإشتراكات</string>
<string name="filters">الفلاتر</string>
<string name="subscription_id_length">طول معرّف الاشتراك</string>
<string name="minimum_prefix">الحد الأدنى لل prefix </string>
<string name="maximum_event_tags">الحد الأقصى لوسوم الحدث</string>
<string name="content_length">طول المحتوى</string>
<string name="minimum_pow">الحد الأدنى لل PoW</string>
<string name="auth">المصادقة</string>
<string name="payment">الدفع</string>
<string name="cashu">Cashu Token</string>
<string name="geohash_title">عرض الموقع كـ </string>
<string name="new_feature_nip24_might_not_be_available_title">ميزة جديدة</string>
<string name="new_feature_nip24_activate">تفعيل</string>
<string name="messages_create_public_chat">عام</string>
<string name="messages_new_message">خاص</string>
<string name="messages_new_message_to">إلى</string>
<string name="messages_new_message_subject">الموضوع</string>
<string name="messages_new_message_subject_caption">موضوع المحادثة</string>
<string name="messages_new_message_to_caption">"\@User1, @User2, @User3"</string>
<string name="messages_group_descriptor">أعضاء هذه المجموعة</string>
<string name="messages_new_subject_message">التوضيح للأعضاء</string>
<string name="messages_new_subject_message_placeholder">تغيير اسم الأهداف الجديدة.</string>
<string name="language_description">من أجل واجهة التطبيق</string>
<string name="theme_description">مظهر غامق أو فاتح أو اوتوماتيكي</string>
<string name="automatically_load_images_gifs_description">تحميل الصور و الصور المتحركة تلقائياً</string>
<string name="automatically_play_videos_description">تشغيل الفيديوهات و الصور المتحركة تلقائياً</string>
<string name="automatically_show_url_preview_description">إظهار معاينات الروابط</string>
<string name="load_image_description">متى يتم تحميل الصور</string>
<string name="copy_url_to_clipboard">نسخ عنوان الرابط إلى الحافظة</string>
<string name="copy_the_note_id_to_the_clipboard">انسخ معرف النص (ID) إلى الحافظة</string>
<string name="created_at">أنشئ في</string>
<string name="rules">القواعد</string>
<string name="status_update">تحديث الحالة</string>
</resources>

View File

@@ -0,0 +1,499 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">QR কোডের দিকে নির্দেশ করুন</string>
<string name="show_qr">QR দেখান</string>
<string name="profile_image">প্রোফাইল ছবি</string>
<string name="scan_qr">QR স্ক্যান করুন</string>
<string name="show_anyway">যাহোক, দেখান</string>
<string name="post_was_flagged_as_inappropriate_by">পোস্টটি কারো দ্বারা অভিযুক্ত হয়েছে</string>
<string name="post_not_found">ইভেন্টটি লোড হচ্ছে অথবা আপনার রিলে তালিকায় এটি পাওয়া যাচ্ছে না</string>
<string name="channel_image">চ্যানেল ইমেজ</string>
<string name="referenced_event_not_found">উল্লেখিত ইভেন্টটি পাওয়া যায় নি</string>
<string name="could_not_decrypt_the_message">মেসেজটি ডিক্রিপ্ট করা যায় নি</string>
<string name="group_picture">দলগত ছবি</string>
<string name="explicit_content">খোলামেলা আধেয়</string>
<string name="spam">স্প্যাম</string>
<string name="impersonation">ছদ্মবেশ</string>
<string name="illegal_behavior">বেআইনী আচরণ</string>
<string name="unknown">অচেনা</string>
<string name="relay_icon">রিলে আইকন</string>
<string name="unknown_author">অজ্ঞাত-পরিচয় লেখক</string>
<string name="copy_text">টেক্সটটি কপি করুন</string>
<string name="copy_user_pubkey">লেখক আইডি কপি করুন</string>
<string name="copy_note_id">নোটের আইডি কপি করুন</string>
<string name="broadcast">সম্প্রচার</string>
<string name="request_deletion">নোটটি মুছে ফেলার অনুরোধ করুন</string>
<string name="block_report">ব্লক/অভিযুক্ত করুন</string>
<string name="block_hide_user"><![CDATA[ব্লক করুন এবং ব্যবহারকারীকে আড়ালে রাখুন]]></string>
<string name="report_spam_scam">স্প্যাম/প্রতারণার অভিযোগ করুন</string>
<string name="report_impersonation">ছদ্মবেশ ধারণের অভিযোগ করুন</string>
<string name="report_explicit_content">খোলামেলা আধেয় হিসেবে অভিযোগ করুন</string>
<string name="report_illegal_behaviour">বেআইনী আচরণের অভিযোগ করুন</string>
<string name="login_with_a_private_key_to_be_able_to_reply">প্রতিউত্তর দিতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">পোস্টগুলি বুস্ট করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
<string name="login_with_a_private_key_to_like_posts">পোস্টগুলি লাইক করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
<string name="no_zap_amount_setup_long_press_to_change">জ্যাপের কোনো পরিমাণ নির্দিষ্ট করা নেই। এটি পরিবর্তন করতে চাইলে দীর্ঘক্ষণ চাপুন</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">কাউকে জ্যাপ পাঠাতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
<string name="login_with_a_private_key_to_be_able_to_follow">কাউকে অনুসরণ করতে ব্যক্তিগত চাবি দিয়ে লগইন করুন</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">কাউকে আনফলো করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
<string name="zaps">জ্যাপস</string>
<string name="view_count">পরিমাণ দেখুন</string>
<string name="boost">বুস্ট</string>
<string name="boosted">বুস্ট করা হয়েছে</string>
<string name="quote">উদ্ধৃতি দিন</string>
<string name="new_amount_in_sats">স্যাটে নতুন পরিমাণ যুক্ত হয়েছে</string>
<string name="add">যুক্ত করুন</string>
<string name="replying_to">"এই ব্যক্তির প্রতিউত্তর দিচ্ছেন "</string>
<string name="and">" এবং "</string>
<string name="in_channel">"চ্যানেলের ভেতর "</string>
<string name="profile_banner">প্রোফাইলের ব্যানার</string>
<string name="payment_successful">সফলভাবে মূল্য পরিশোধিত হয়েছে</string>
<string name="error_parsing_error_message">এরর পার্সিং এরর মেসেজ</string>
<string name="following">" অনুসরণ"</string>
<string name="followers">" অনুসারী"</string>
<string name="profile">প্রোফাইল</string>
<string name="security_filters">নিরাপত্তা-ফিল্টার</string>
<string name="log_out">লগ আউট</string>
<string name="show_more">আরও দেখান</string>
<string name="lightning_invoice">লাইটনিং ইনভয়েস</string>
<string name="pay">পরিশোধ করুন</string>
<string name="lightning_tips">লাইটনিং টিপস</string>
<string name="note_to_receiver">প্রাপকের উদ্দেশে চিরকুট</string>
<string name="thank_you_so_much">আপনাকে অনেক ধন্যবাদ!</string>
<string name="amount_in_sats">স্যাটে মোট পরিমাণ</string>
<string name="send_sats">স্যাট পাঠান</string>
<string name="error_parsing_preview_for">"এই দুইয়ের পূর্বরূপ দেখানো যাচ্ছে না %1$s : %2$s"</string>
<string name="preview_card_image_for">"%1$s এর জন্য কার্ড ইমেজের পূর্বরূপ দেখুন"</string>
<string name="new_channel">নতুন চ্যানেল</string>
<string name="channel_name">চ্যানেলের নাম</string>
<string name="my_awesome_group">আমার চম‍‌‌‍ৎকার গ্রুপ</string>
<string name="picture_url">ছবির Url</string>
<string name="description">বিবরণ</string>
<string name="about_us">"আমাদের সম্পর্কে.. "</string>
<string name="what_s_on_your_mind">নিজের কোনো ভাবনা প্রকাশ করতে চান?</string>
<string name="post">পেশ করুন</string>
<string name="save">সংরক্ষণ করুন</string>
<string name="create">তৈরি করুন</string>
<string name="cancel">বাতিল করুন</string>
<string name="failed_to_upload_the_image">ছবিটি সফলভাবে আপলোড করা যায় নি</string>
<string name="relay_address">রিলে ঠিকানা</string>
<string name="posts">পোস্টগুলি</string>
<string name="bytes">বাইটস</string>
<string name="errors">ত্রুটিসমূহ</string>
<string name="home_feed">নীড়পাতা</string>
<string name="private_message_feed">ব্যক্তিগত বার্তাসমূহ</string>
<string name="public_chat_feed">সার্বজনীন কথোপকথন</string>
<string name="global_feed">বৈশ্বিক পাতা</string>
<string name="search_feed">অনুসন্ধান পাতা</string>
<string name="add_a_relay">একটি রিলে যুক্ত করুন</string>
<string name="display_name">প্রদর্শিত নাম</string>
<string name="my_display_name">আমার প্রদর্শিত নাম</string>
<string name="username">ইউজারনেম</string>
<string name="my_username">আমার ইউজারনেম</string>
<string name="about_me">আমার সম্পর্কে</string>
<string name="avatar_url">অবতারের URL</string>
<string name="banner_url">ব্যানারের URL</string>
<string name="website_url">ওয়েবসাইটের URL</string>
<string name="ln_address">LN ঠিকানা</string>
<string name="ln_url_outdated">LN URL (অপ্রচলিত)</string>
<string name="image_saved_to_the_gallery">ছবিটি গ্যালারিতে সংরক্ষণ করা হয়েছে</string>
<string name="failed_to_save_the_image">ছবিটি সফলভাবে সংরক্ষণ করা যায় নি</string>
<string name="upload_image">ছবিটি আপলোড করুন</string>
<string name="uploading">আপলোড করা হচ্ছে…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">স্যাট গ্রহণের জন্য ব্যবহারকারীর কোনো লাইটনিং ঠিকানা দেওয়া নেই</string>
<string name="reply_here">"এখানে প্রতিউত্তর দিন.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">নস্টারে শেয়ারের জন্য নোট আইডিটি ক্লিপবোর্ডে কপি করা হয়</string>
<string name="copy_channel_id_note_to_the_clipboard">চ্যানেল আইডি (নোট) ক্লিপবোর্ডে কপি করুন</string>
<string name="edits_the_channel_metadata">চ্যানেলের মেটাডেটা সম্পাদনা করে</string>
<string name="join">যুক্ত হোন</string>
<string name="known">জ্ঞাত</string>
<string name="new_requests">নয়া অনুরোধসমূহ</string>
<string name="blocked_users">ব্লক করে রাখা ব্যবহারকারীরা</string>
<string name="new_threads">নতুন থ্রেড</string>
<string name="conversations">কথোপকথন</string>
<string name="notes">নোটস</string>
<string name="replies">প্রতিউত্তরসমূহ</string>
<string name="follows">"অনুসারী"</string>
<string name="reports">"অভিযোগসমূহ"</string>
<string name="more_options">আরও বিকল্প</string>
<string name="relays">" রিলেগুলি"</string>
<string name="website">ওয়েবসাইট</string>
<string name="lightning_address">লাইটনিং ঠিকানা</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">ব্যাকআপের জন্য আপনার Nsec আইডি (তথা পাসওয়ার্ড) ক্লিপবোর্ডে কপি করে</string>
<string name="copy_private_key_to_the_clipboard">আপনার ব্যক্তিগত চাবিটি ক্লিপবোর্ডে কপি করুন</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">অন্যদের সাথে শেয়ারের জন্য আপনার গণ চাবি ক্লিপবোর্ডে কপি করে</string>
<string name="copy_public_key_npub_to_the_clipboard">গণ চাবি (Npub) ক্লিপবোর্ডে কপি করুন</string>
<string name="send_a_direct_message">একটি সরাসরি-বার্তা পাঠান</string>
<string name="edits_the_user_s_metadata">ব্যবহারকারীর মেটাডেটা সম্পাদনা করে</string>
<string name="follow">অনুসরণ করুন</string>
<string name="follow_back">প্রতি-অনুসরণ করুন</string>
<string name="unblock">আনব্লক করুন</string>
<string name="copy_user_id">ব্যবহারকারীর আইডি কপি করুন</string>
<string name="unblock_user">ব্যবহারকারীকে আনব্লক করুন</string>
<string name="npub_hex_username">"Npub, ইউজারনেম, টেক্সট"</string>
<string name="clear">মুছে ফেলুন</string>
<string name="app_logo">অ্যাপের লোগো</string>
<string name="nsec_npub_hex_private_key">Nsec.. অথবা Npub..</string>
<string name="show_password">পাসওয়ার্ড দেখান</string>
<string name="hide_password">পাসওয়ার্ড লুকান</string>
<string name="invalid_key">অকেজো কি</string>
<string name="i_accept_the">"আমি এটা মেনে নিচ্ছি "</string>
<string name="terms_of_use">ব্যবহারের শর্তাবলী</string>
<string name="acceptance_of_terms_is_required">প্রদত্ত শর্তাবলী কবুল করা আবশ্যক</string>
<string name="key_is_required">কি প্রয়োজন</string>
<string name="login">লগ ইন</string>
<string name="generate_a_new_key">একটি নতুন কি তৈরি করুন</string>
<string name="loading_feed">ফিডটি লোড করা হচ্ছে</string>
<string name="error_loading_replies">"প্রতিউত্তরগুলি লোড করা যাচ্ছে না: "</string>
<string name="try_again">আবার চেষ্টা করুন</string>
<string name="feed_is_empty">ফিডটি খালি.</string>
<string name="refresh">রিফ্রেশ করুন</string>
<string name="created">তৈরি করা হয়েছে</string>
<string name="with_description_of">এর বিবরণ সহ</string>
<string name="and_picture">এবং ছবি</string>
<string name="changed_chat_name_to">চ্যাটের নাম পরিবর্তন করুন</string>
<string name="description_to">বিবরণ</string>
<string name="and_picture_to">এবং ছবি</string>
<string name="leave">ছেড়ে দিন</string>
<string name="unfollow">আনফলো করুন</string>
<string name="channel_created">চ্যানেলটি তৈরি করা হয়েছে</string>
<string name="channel_information_changed_to">"চ্যানেলের তথ্যের পরিবর্তিত চেহারা"</string>
<string name="public_chat">সার্বজনীন আলাপ</string>
<string name="posts_received">পেশকৃত আধেয় গৃহীত হয়েছে</string>
<string name="remove">সরিয়ে ফেলুন</string>
<string name="translations_auto">স্বয়ংক্রিয়</string>
<string name="translations_translated_from">এই মূলপাঠ থেকে অনূদিত হয়েছে</string>
<string name="translations_to">থেকে</string>
<string name="translations_show_in_lang_first">প্রথমে %1$s ভাষায় দেখান</string>
<string name="translations_always_translate_to_lang">সর্বদা %1$s ভাষায় অনুবাদ করুন</string>
<string name="translations_never_translate_from_lang">কখনোই %1$s ভাষা থেকে অনুবাদ করবেন না</string>
<string name="nip_05">নস্টার ঠিকানা</string>
<string name="never">কখনো না</string>
<string name="now">এখন</string>
<string name="h">h</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="nudity">নগ্নতা</string>
<string name="profanity_hateful_speech">অশ্লীলতা / বিদ্বেষপূর্ণ বক্তব্য</string>
<string name="report_hateful_speech">বিদ্বেষপূর্ণ বক্তব্য হিসেবে অভিযোগ জানান</string>
<string name="report_nudity_porn">নগ্নতা / পর্ন হিসেবে অভিযোগ জানান</string>
<string name="others">অন্যান্য</string>
<string name="mark_all_known_as_read">পরিচিত সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
<string name="mark_all_new_as_read">নতুন সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
<string name="mark_all_as_read">সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
<string name="backup_keys">ব্যাকআপ চাবিগুলি</string>
<string name="account_backup_tips_md" tools:ignore="Typos"> ## চাবির ব্যাকআপ এবং নিরাপত্তা বিষয়ক টিপস
\n\nআপনার অ্যাকাউন্ট একটি ব্যক্তিগত চাবি দ্বারা সুরক্ষিত। চাবি হলো এলোমেলো অক্ষর ও সংখ্যার একটি দীর্ঘ তন্তু যা **nsec** দ্বারা শুরু হয়। আপনার ব্যক্তিগত চাবিটি পেলে যে কেউ আপনার পরিচয় ব্যবহার করে যেকোনো আধেয় প্রকাশ করতে পারবে।
\n\n- আপনি বিশ্বাস করেন না এমন কোনো ওয়েবসাইট কিংবা সফটওয়্যারে আপনার ব্যক্তিগত চাবিটি রাখবেন **না**।
\n- অ্যামেথিস্টের ডেভেলপাররা **কখনোই** আপনার কাছে আপনার ব্যক্তিগত চাবিটি জানতে চাইবে না।
\n- আপনার ব্যক্তিগত চাবির একটি নিরাপদ ব্যাকআপ **অবশ্যই** সংরক্ষণ করবেন কারণ এটি আপনার অ্যাকাউন্ট পুনরুদ্ধারে কাজে আসবে। আমরা একটি পাসওয়ার্ড ম্যানেজার ব্যবহারের পরামর্শ দিই।
</string>
<string name="secret_key_copied_to_clipboard">ব্যক্তিগত চাবি (nsec) ক্লিপবোর্ডে কপি করা হয়েছে</string>
<string name="copy_my_secret_key">আমার ব্যক্তিগত চাবিটি কপি করুন</string>
<string name="biometric_authentication_failed">প্রমাণীকরণ ব্যর্থ হয়েছে।</string>
<string name="biometric_error">ত্রুটি</string>
<string name="badge_created_by">"%1$s দ্বারা নির্মিত"</string>
<string name="badge_award_image_for">"%1$s -এর জন্য ব্যাজ পুরষ্কারের ছবি"</string>
<string name="new_badge_award_notif">আপনি একটি নতুন ব্যাজ পুরষ্কার পেয়েছেন</string>
<string name="award_granted_to">ব্যাজ পুরস্কারটি পাচ্ছে</string>
<string name="copied_note_text_to_clipboard">নোটের রচনাটি ক্লিপবোর্ডে কপি করা হয়েছে</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">রচয়িতার @npub ক্লিপবোর্ডে কপি করুন</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">নোট আইডিটি (@note1) ক্লিপবোর্ডে কপি করা হয়েছে</string>
<string name="select_text_dialog_top">রচনাটি নির্বাচন করুন</string>
<string name="private_conversation_notification">"&lt;Unable to decrypt private message&gt;\n\nআপনাকে %1$s এবং %2$s এর মধ্যে একটি ব্যক্তিগত/এনক্রিপ্টেড কথোপকথনে উদ্ধৃত করা হয়েছে৷"</string>
<string name="account_switch_add_account_dialog_title">নতুন অ্যাকাউন্ট যুক্ত করুন</string>
<string name="drawer_accounts">অ্যাকাউন্টগুলি</string>
<string name="account_switch_select_account">অ্যাকাউন্ট নির্বাচন করুন</string>
<string name="account_switch_add_account_btn">নতুন অ্যাকাউন্ট যুক্ত করুন</string>
<string name="account_switch_active_account">অ্যাকাউন্ট সক্রিয় করুন</string>
<string name="account_switch_has_private_key">ব্যক্তিগত চাবি আছে</string>
<string name="account_switch_pubkey_only">শুধুমাত্র পাঠযোগ্য, ব্যক্তিগত চাবি নেই</string>
<string name="back">পিছে যান</string>
<string name="quick_action_select">নির্বাচন করুন</string>
<string name="quick_action_share_browser_link">ব্রাউজারের লিংক শেয়ার করুন</string>
<string name="quick_action_share">শেয়ার</string>
<string name="quick_action_copy_user_id">লেখকের আইডি</string>
<string name="quick_action_copy_note_id">নোটের আইডি</string>
<string name="quick_action_copy_text">টেক্সটটি কপি করুন</string>
<string name="quick_action_delete">মুছে ফেলুন</string>
<string name="quick_action_unfollow">আনফলো করুন</string>
<string name="quick_action_follow">অনুসরণ করুন</string>
<string name="quick_action_request_deletion_alert_title">নোটটি মুছে ফেলার অনুরোধ করুন</string>
<string name="quick_action_request_deletion_alert_body">অ্যামেথিস্ট অনুরোধ করবে যেন আপনার নোটটি আপনার বর্তমানে সংযুক্ত রিলেগুলো থেকে মুছে ফেলা হয়। তবে কোনো নিশ্চয়তা নেই যে নোটটি স্থায়ীভাবে সেই রিলেগুলি থেকে বা অন্যান্য রিলে থেকে মুছে ফেলা হবে, যেখানে এটি সংরক্ষণ করা হতে পারে৷</string>
<string name="quick_action_block_dialog_btn">ব্লক করুন</string>
<string name="quick_action_delete_dialog_btn">মুছে ফেলুন</string>
<string name="quick_action_block">ব্লক করুন</string>
<string name="quick_action_report">অভিযোগ করুন</string>
<string name="quick_action_delete_button">মুছে ফেলুন</string>
<string name="quick_action_dont_show_again_button">আর দেখাবেন না</string>
<string name="report_dialog_spam">স্প্যাম অথবা প্রতারণা</string>
<string name="report_dialog_profanity">অশ্লীলতা বা বিদ্বেষপূর্ণ আচরণ</string>
<string name="report_dialog_impersonation">ক্রূর উদ্দেশ্য সাধনে ছদ্মবেশ</string>
<string name="report_dialog_nudity">নগ্নতা বা গ্রাফিক আধেয়</string>
<string name="report_dialog_illegal">বেআইনী আচরণ</string>
<string name="report_dialog_blocking_a_user">কোনো ব্যবহারকারীকে ব্লক করে রাখলে আপনার অ্যাপে তাদের কোনো আধেয় দেখা যাবে না। তবে আপনার নোটগুলি সর্বজনীনভাবে উন্মুক্ত, আপনার দ্বারা ব্লক করা ব্যক্তিরাও তা দেখতে পাবে। ব্লক করে রাখা ব্যবহারকারীদেরকে নিরাপত্তা-ফিল্টার স্ক্রিনে তালিকাভুক্ত করা হয়েছে।</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[ব্যবহারকারীকে ব্লক করুন এবং আড়ালে রাখুন]]></string>
<string name="report_dialog_report_btn">অবমাননার অভিযোগ করুন</string>
<string name="report_dialog_reminder_public">পেশকৃত সমস্ত অভিযোগ সর্বজনীনভাবে দৃশ্যমান হবে৷</string>
<string name="report_dialog_additional_reason_placeholder">আপনি চাইলে আপনার অভিযোগের সাথে অতিরিক্ত অনুসঙ্গ যোগ করতে পারেন…</string>
<string name="report_dialog_additional_reason_label">সংযোজিত অনুসঙ্গ</string>
<string name="report_dialog_select_reason_label">কারণ</string>
<string name="report_dialog_select_reason_placeholder">একটি কারণ নির্বাচন করুন…</string>
<string name="report_dialog_post_report_btn">অভিযোগ পেশ করুন</string>
<string name="report_dialog_title">ব্লক করুন এবং অভিযোগ পেশ করুন</string>
<string name="block_only">ব্লক করুন</string>
<string name="bookmarks">বুকমার্ক করুন</string>
<string name="private_bookmarks">ব্যক্তিগত বুকমার্ক</string>
<string name="public_bookmarks">সর্বজনীন বুকমার্ক</string>
<string name="add_to_private_bookmarks">ব্যক্তিগত বুকমার্কে যুক্ত করুন</string>
<string name="add_to_public_bookmarks">সর্বজনীন বুকমার্কে যুক্ত করুন</string>
<string name="remove_from_private_bookmarks">ব্যক্তিগত বুকমার্ক থেকে সরিয়ে দিন</string>
<string name="remove_from_public_bookmarks">সর্বজনীন বুকমার্ক থেকে সরিয়ে দিন</string>
<string name="wallet_connect_service">ওয়ালেট সংযোগের পরিষেবা</string>
<string name="wallet_connect_service_explainer">অ্যাপ থেকে বের না হয়েই জ্যাপ প্রদান করতে একটি নস্টার-সিক্রেট অনুমোদন করা হয়ে থাকে। এই নস্টার-সিক্রেটটি নিরাপদে রাখুন এবং সম্ভব হলে একটি প্রাইভেট রিলে ব্যবহার করুন</string>
<string name="wallet_connect_service_pubkey">ওয়ালেট সংযোগের গণ চাবি</string>
<string name="wallet_connect_service_relay">ওয়ালেট সংযোগের রিলে</string>
<string name="wallet_connect_service_secret">ওয়ালেট সংযোগের সিক্রেট</string>
<string name="wallet_connect_service_show_secret">গোপন চাবি দেখান</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex ব্যক্তিগত চাবি</string>
<string name="pledge_amount_in_sats">প্রতিশ্রুত পরিমাণ স্যাট দ্বারা জানান</string>
<string name="post_poll">একটি পোল পেশ করুন</string>
<string name="poll_heading_required">প্রয়োজনীয় ক্ষেত্রসমূহ:</string>
<string name="poll_zap_recipients">জ্যাপ প্রাপকেরা</string>
<string name="poll_primary_description">পোলের প্রাথমিক বিবরণ…</string>
<string name="poll_option_index">বিকল্প %s</string>
<string name="poll_option_description">পোলের বিবিধ বিকল্পের বিবরণ</string>
<string name="poll_heading_optional">ঐচ্ছিক ক্ষেত্র:</string>
<string name="poll_zap_value_min">ন্যূনতম জ্যাপ</string>
<string name="poll_zap_value_max">সর্বোচ্চ জ্যাপ</string>
<string name="poll_consensus_threshold">ঐক্যমত</string>
<string name="poll_consensus_threshold_percent">(০–১০০)%</string>
<string name="poll_closing_time">এই সময় পর বন্ধ করুন</string>
<string name="poll_closing_time_days">দিনগুলি</string>
<string name="poll_is_closed">পোলটি আর নতুন ভোট গ্রহণ করবে না</string>
<string name="poll_zap_amount">পরিমাণটি জ্যাপ করুন</string>
<string name="one_vote_per_user_on_atomic_votes">এই জাতীয় পোলে একজন ব্যবহারকারী শুধুমাত্র একটি করেই ভোট দিতে পারবেন</string>
<string name="looking_for_event">"ইভেন্ট খোঁজা হচ্ছে %1$s"</string>
<string name="custom_zaps_add_a_message">একটি সর্বজনীন বার্তা পাঠান</string>
<string name="custom_zaps_add_a_message_private">একটি ব্যক্তিগত বার্তা পাঠান</string>
<string name="custom_zaps_add_a_message_nonzap">একটি ইনভয়েস বার্তা পাঠান</string>
<string name="custom_zaps_add_a_message_example">আপনার সকল কাজের জন্য আপনাকে অসংখ্য ধন্যবাদ!</string>
<string name="lightning_create_and_add_invoice">তৈরি করুন এবং যোগ করুন</string>
<string name="poll_author_no_vote">পোল-রচয়িতারা নিজেদের পোলে ভোট দিতে পারবেন না।</string>
<string name="hash_verification_passed">ঐ পোস্ট এবং এই আধেয়র বিষয়বস্তু অভিন্ন</string>
<string name="hash_verification_failed">এই আধেয়র বিষয়বস্তু পরিবর্তিত হয়েছে। লেখক হয়তো পরিবর্তনটি দেখেন নি বা অনুমোদন করেন নি</string>
<string name="content_description_add_image">ছবি যোগ করুন</string>
<string name="content_description_add_video">ভিডিও যোগ করুন</string>
<string name="content_description_add_document">নথি যোগ করুন</string>
<string name="add_content">বার্তায় যোগ করুন</string>
<string name="content_description">বিষয়বস্তুর বিবরণ</string>
<string name="content_description_example">সূর্যাস্তের সময় সাদা বালুকাময় সমুদ্র সৈকতে একটি নীল নৌকা</string>
<string name="zap_type">জ্যাপের ধরন</string>
<string name="zap_type_explainer">সকল বিকল্পগুলিতে জ্যাপের ধরন</string>
<string name="zap_type_public">সর্বজনীন</string>
<string name="zap_type_public_explainer">লেনদেন এবং বার্তা সকলে দেখতে পারেন</string>
<string name="zap_type_private">ব্যক্তিগত</string>
<string name="zap_type_private_explainer">প্রেরক এবং প্রাপক একে অপরকে দেখতে এবং বার্তা পড়তে পারেন</string>
<string name="zap_type_anonymous">অজ্ঞাত-পরিচয়</string>
<string name="zap_type_anonymous_explainer">প্রাপক এবং জনসাধারণ জানেন না কে প্রদত্ত অর্থ পাঠিয়েছেন</string>
<string name="zap_type_nonzap">নন-জ্যাপ</string>
<string name="zap_type_nonzap_explainer">নস্টারে কোনো সন্ধান নেই, কেবল লাইটনিংয়ে আছে</string>
<string name="file_server">ফাইল সার্ভার</string>
<string name="zap_forward_lnAddress">LN ঠিকানা বা @ইউজার</string>
<string name="upload_server_imgur">imgur.com - বিশ্বস্ত</string>
<string name="upload_server_imgur_explainer">Imgur ফাইলটি পরিবর্তন করতে পারে</string>
<string name="upload_server_nostrimg">nostrimg.com - বিশ্বস্ত</string>
<string name="upload_server_nostrimg_explainer">NostrImg ফাইলটি পরিবর্তন করতে পারে</string>
<string name="upload_server_nostrbuild">nostr.build - বিশ্বস্ত</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build ফাইলটি পরিবর্তন করতে পারে</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - বিশ্বস্ত</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করতে পারে</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - বিশ্বস্ত</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me ফাইলটি পরিবর্তন করতে পারে</string>
<string name="upload_server_imgur_nip94">যাচাইযোগ্য Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Imgur ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
<string name="upload_server_nostrimg_nip94">যাচাইযোগ্য NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">NostrImg ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
<string name="upload_server_nostrbuild_nip94">যাচাইযোগ্য Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.build ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
<string name="upload_server_nostrfilesdev_nip94">যাচাইযোগ্য Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
<string name="upload_server_nostrcheckme_nip94">যাচাইযোগ্য Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.me ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
<string name="upload_server_relays_nip95">আপনার রিলেগুলি (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">ফাইলগুলো আপনার রিলে দ্বারা গৃহীত হয়। নতুন NIP: তারা এটি সমর্থন করে কিনা দেখে নিন</string>
<string name="connect_via_tor_short">Tor/Orbot সেটআপ করুন</string>
<string name="connect_via_tor">আপনার Orbot সেটআপের মাধ্যমে সংযুক্ত হোন</string>
<string name="do_you_really_want_to_disable_tor_title">Orbot/Tor থেকে সংযোগ বিচ্ছিন্ন করতে চান?</string>
<string name="do_you_really_want_to_disable_tor_text">আপনার ডেটা অবিলম্বে নিয়মিত নেটওয়ার্কে স্থানান্তর করা হবে</string>
<string name="yes">হ্যাঁ</string>
<string name="no">না</string>
<string name="follow_list_selection">অনুসরণ তালিকা</string>
<string name="follow_list_kind3follows">অনুসৃত সকলেরা</string>
<string name="follow_list_global">বৈশ্বিক</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Tor এর মাধ্যমে Orbot এর সাথে সংযুক্ত হোন
\n\n১. [Orbot] নামান (https://play.google.com/store/apps/details?id=org.torproject.android)
\n২. Orbot চালু করুন
\n৩. Orbot এ ঢুকে Socks পোর্টটি খেয়াল করুন। এটি ৯০৫০ তে নির্দিষ্ট করা থাকে
\n. প্রয়োজনে Orbot এ পোর্টটি বদলে দিন
\n৫. স্ক্রিনের Socks পোর্টটি প্রস্তুত করুন
\n৬. Orbot কে প্রক্সি হিসেবে ব্যবহার করতে Activate বোতামটি চাপুন
</string>
<string name="orbot_socks_port">Orbot এর Socks পোর্ট</string>
<string name="invalid_port_number">পোর্ট নম্বরটি অকার্যকর</string>
<string name="use_orbot">Orbot ব্যবহার করুন</string>
<string name="disconnect_from_your_orbot_setup">Tor/Orbot থেকে সংযোগ বিচ্ছিন্ন করুন</string>
<string name="app_notification_dms_channel_name">ব্যক্তিগত বার্তাসমূহ</string>
<string name="app_notification_dms_channel_description">যখন একটি ব্যক্তিগত বার্তা আসে তখন আপনাকে অবহিত করে</string>
<string name="app_notification_zaps_channel_name">জ্যাপ গৃহীত হয়েছে</string>
<string name="app_notification_zaps_channel_description">কেউ জ্যাপ পাঠালে আপনাকে অবহিত করে</string>
<string name="app_notification_zaps_channel_message">%1$s স্যাট</string>
<string name="app_notification_zaps_channel_message_from">%1$s থেকে</string>
<string name="app_notification_zaps_channel_message_for">%1$s এর জন্য</string>
<string name="reply_notify">অবহিত করুন: </string>
<string name="channel_list_join_conversation">আলোচনায় যুক্ত হোন</string>
<string name="channel_list_user_or_group_id">ব্যবহারকারী কিংবা গ্রুপের আইডি</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent অথবা hex</string>
<string name="channel_list_create_channel">তৈরি করুন</string>
<string name="channel_list_join_channel">যুক্ত হোন</string>
<string name="today">আজ</string>
<string name="content_warning">আধেয় বিষয়ক সতর্কতা</string>
<string name="content_warning_explanation">এই পোস্টে সংবেদনশীল উপাদান রয়েছে যা কারো জন্য আপত্তিকর কিংবা সমস্যাজনক হতে পারে</string>
<string name="content_warning_hide_all_sensitive_content">সংবেদনশীল আধেয় সবসময় আড়ালে রাখুন</string>
<string name="content_warning_show_all_sensitive_content">সংবেদনশীল বিষয়বস্তু সবসময় খোলামেলা দেখান</string>
<string name="content_warning_see_warnings">সবসময় আধেয় বিষয়ক সতর্কতা দেখান</string>
<string name="recommended_apps">প্রস্তাবিত: </string>
<string name="filter_spam_from_strangers">অপরিচিতদের কাছ থেকে স্প্যাম ফিল্টার করুন</string>
<string name="warn_when_posts_have_reports_from_your_follows">কোনো পোস্টে আপনার অনুসরীদের থেকে অভিযোগ থাকলে অবহিত হোন</string>
<string name="new_reaction_symbol">প্রতিক্রিয়ার নতুন প্রতীক</string>
<string name="no_reaction_type_setup_long_press_to_change">প্রতিক্রিয়ার কোনো ধরন বেছে নেওয়া হয়নি। পরিবর্তন করতে দীর্ঘক্ষণ চাপুন</string>
<string name="zapraiser">জ্যাপরেইজার</string>
<string name="zapraiser_explainer">এই পোস্টের জন্য স্যাটের একটি লক্ষ্যমাত্রা যোগ করে। সহায়তাকারী ক্লায়েন্টরা অনুদানকে উৎসাহিত করার জন্য একে একটি অগ্রগতির মানদণ্ড হিসাবে দেখাতে পারে</string>
<string name="zapraiser_target_amount_in_sats">পরিমাণের লক্ষ্যমাত্রা স্যাট দ্বারা জানান</string>
<string name="sats_to_complete">জ্যাপরাইজার %1$s তে আছে। %2$s স্যাট লক্ষ্যমাত্রা</string>
<string name="read_from_relay">রিলে থেকে পড়ুন</string>
<string name="write_to_relay">রিলেতে লিখুন</string>
<string name="an_error_occurred_trying_to_get_relay_information">%1$s থেকে রিলের তথ্যের জন্য চেষ্টা করার সময় একটি ত্রুটি ঘটেছে৷</string>
<string name="owner">মালিক</string>
<string name="version">সংস্করণ</string>
<string name="software">সফটওয়্যার</string>
<string name="contact">যোগাযোগ</string>
<string name="supports">সমর্থিত NIP গুলো</string>
<string name="admission_fees">প্রবেশমূল্য</string>
<string name="payments_url">মূল্যপরিশোধের URL</string>
<string name="limitations">সীমাবদ্ধতা</string>
<string name="countries">দেশসমূহ</string>
<string name="languages">ভাষাসমূহ</string>
<string name="tags">ট্যাগগুলি</string>
<string name="posting_policy">আধেয় পেশ করার নীতি</string>
<string name="message_length">বার্তার ব্যাপ্তি</string>
<string name="subscriptions">সকল সদস্যতা</string>
<string name="filters">ফিল্টারগুলি</string>
<string name="subscription_id_length">সদস্যতা আইডির ব্যাপ্তি</string>
<string name="minimum_prefix">ন্যূনতম উপসর্গ</string>
<string name="maximum_event_tags">সর্বাধিক ইভেন্ট ট্যাগ</string>
<string name="content_length">আধেয়র ব্যাপ্তি</string>
<string name="minimum_pow">ন্যূনতম PoW</string>
<string name="auth">অনুমোদন</string>
<string name="payment">মূল্যপরিশোধ</string>
<string name="cashu">ক্যাশু টোকেন</string>
<string name="cashu_redeem">খালাস করুন</string>
<string name="no_lightning_address_set">কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
<string name="copied_token_to_clipboard">টোকেনটি ক্লিপবোর্ডে কপি করা হয়েছে</string>
<string name="live_stream_live_tag">লাইভ</string>
<string name="live_stream_offline_tag">অফলাইন</string>
<string name="live_stream_ended_tag">সমাপ্ত</string>
<string name="live_stream_planned_tag">সময়সূচি নির্ধারিত</string>
<string name="live_stream_is_offline">লাইভস্ট্রিমটি অফলাইন আছে</string>
<string name="live_stream_has_ended">লাইভস্ট্রিম সমাপ্ত</string>
<string name="are_you_sure_you_want_to_log_out">লগ আউট করলে আপনার সমস্ত স্থানীয় তথ্য মুছে যাবে। অ্যাকাউন্ট সুরক্ষিত রাখতে আপনার ব্যক্তিগত চাবিটি নিরাপদে সংরক্ষিত আছে কিনা নিশ্চিত হোন। সামনে আগাতে চান?</string>
<string name="followed_tags">অনুসৃত ট্যাগগুলি</string>
<string name="relay_setup">রিলেগুলি</string>
<string name="discover_live">লাইভ</string>
<string name="discover_community">কম্যুনিটি</string>
<string name="discover_chat">বার্তালাপগুলি</string>
<string name="community_approved_posts">অনুমোদিত পোস্টগুলি</string>
<string name="groups_no_descriptor">এই দলটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে দলনেতার সাথে কথা বলুন</string>
<string name="community_no_descriptor">এই কম্যুনিটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে এর মালিকের সাথে কথা বলুন</string>
<string name="add_sensitive_content_label">সংবেদনশীল আধেয়</string>
<string name="add_sensitive_content_description">সংবেদনশীল আধেয় দেখানোর আগে এ সম্পর্কিত সতর্কতা প্রদর্শন করে</string>
<string name="settings">সেটিংস</string>
<string name="connectivity_type_always">সবসময়</string>
<string name="connectivity_type_wifi_only">শুধুমাত্র ওয়াইফাইতে</string>
<string name="connectivity_type_never">কখনো না</string>
<string name="system">সিস্টেম</string>
<string name="light">উজ্জ্বল</string>
<string name="dark">আঁধারি</string>
<string name="application_preferences">অ্যাপ্লিকেশনের পছন্দসমূহ</string>
<string name="language">ভাষা</string>
<string name="theme">থিম</string>
<string name="automatically_load_images_gifs">ছবির পূর্বরূপ</string>
<string name="automatically_play_videos">ভিডিও প্লেব্যাক</string>
<string name="automatically_show_url_preview">URL-এর পূর্বরূপ</string>
<string name="load_image">ছবিটি লোড করুন</string>
<string name="spamming_users">স্প্যামাররা</string>
<string name="muted_button">মৌন করে রাখা। এটি তুলে নিতে ক্লিক করুন</string>
<string name="mute_button">শব্দ চালু আছে। মৌন করতে ক্লিক করুন</string>
<string name="search_button">স্থানীয় এবং দূরবর্তী রেকর্ড অনুসন্ধান করুন</string>
<string name="nip05_verified">নস্টার ঠিকানাটি যাচাই করা হয়েছে</string>
<string name="nip05_failed">নস্টার ঠিকানাটি যাচাই করা যায় নি</string>
<string name="nip05_checking">নস্টার ঠিকানাটি পরীক্ষা করে দেখা হচ্ছে</string>
<string name="select_deselect_all">সবগুলিকে নির্বাচন/অনির্বাচন করুন</string>
<string name="default_relays">পূর্ব-নির্ধারিত</string>
<string name="select_a_relay_to_continue">চালিয়ে যেতে একটি রিলে নির্বাচন করুন</string>
<string name="zap_forward_title">জ্যাপটি পাঠান:</string>
<string name="zap_forward_explainer">সহায়তাকারী ক্লায়েন্টরা আপনার পরিবর্তে নীচের LN ঠিকানা বা ব্যবহারকারীর প্রোফাইলে জ্যাপ পাঠাবে</string>
<string name="geohash_title">আপনার অবস্থান এভাবে প্রকাশ করুন </string>
<string name="geohash_explainer">পোস্টে আপনার অবস্থানের একটি জিওহ্যাশ যোগ করে। জনসাধারণ জানবে যে আপনি বর্তমান অবস্থান থেকে ৫ কিলোমিটার (৩ মাইল) এর মধ্যে আছেন</string>
<string name="add_sensitive_content_explainer">আপনার পেশকৃত আধেয় দেখানোর আগে সংবেদনশীল আধেয়-সতর্কতা জারি করে। এটি সেসকল আধেয়র জন্য আদর্শ যেখানে NSFW উপাদান কিংবা এমনকিছু থাকতে পারে যা কারো কারো জন্য আপত্তিকর কিংবা সমস্যাজনক মনে হতে পারে।</string>
<string name="new_feature_nip24_might_not_be_available_title">নতুন ফিচার</string>
<string name="new_feature_nip24_might_not_be_available_description">এই মোড সক্রিয় করার জন্য অ্যামেথিস্টকে একটি NIP-24 বার্তা পাঠাতে হবে। (GiftWrapped, সিল করা সরাসরি এবং দলগত বার্তা)। NIP-24 নতুন এবং বেশিরভাগ ক্লায়েন্ট এখনও এটি বাস্তবায়ন করেনি। নিশ্চিত করুন যে প্রাপক একটি সামঞ্জস্যপূর্ণ ক্লায়েন্ট ব্যবহার করছে।</string>
<string name="new_feature_nip24_activate">সক্রিয় করুন</string>
<string name="messages_create_public_chat">সর্বজনীন</string>
<string name="messages_new_message">ব্যক্তিগত</string>
<string name="messages_new_message_to">প্রতি</string>
<string name="messages_new_message_subject">বিষয়</string>
<string name="messages_new_message_subject_caption">আলোচনার বিষয়</string>
<string name="messages_new_message_to_caption">"\@User1, @User2, @User3"</string>
<string name="messages_group_descriptor">এই দলের সদস্যরা</string>
<string name="messages_new_subject_message">সদস্যদের নিকট ব্যাখ্যা</string>
<string name="messages_new_subject_message_placeholder">নতুন লক্ষ্যের জন্য নাম পরিবর্তন করা হচ্ছে।</string>
<string name="language_description">অ্যাপের ইন্টারফেসের জন্য</string>
<string name="theme_description">আঁধারি, উজ্জ্বল কিংবা সিস্টেমের থিম</string>
<string name="automatically_load_images_gifs_description">স্বয়ংক্রিয়ভাবে ছবি এবং GIF লোড করুন</string>
<string name="automatically_play_videos_description">স্বয়ংক্রিয়ভাবে ভিডিও এবং GIF চালু করুন</string>
<string name="automatically_show_url_preview_description">URL এর পূর্বরূপ দেখান</string>
<string name="load_image_description">কখন ছবি লোড করতে হবে</string>
<string name="copy_url_to_clipboard">URL ক্লিপবোর্ডে কপি করুন</string>
<string name="copy_the_note_id_to_the_clipboard">নোট আইডি ক্লিপবোর্ডে কপি করুন</string>
<string name="created_at">যেখানে নির্মিত</string>
<string name="rules">নিয়ম</string>
<string name="status_update">আপনার স্ট্যাটাস আপডেট করুন</string>
<string name="lightning_wallets_not_found">Error parsing error message</string>
<string name="poll_zap_value_min_max_explainer">জ্যাপের পরিমাণ দ্বারা ভোটসংখ্যা গোণা হয়। আপনি স্প্যামার এড়াতে একটি সর্বনিম্ন পরিমাণ এবং একটি বড়ো জ্যাপার দ্বারা পোল দখল এড়াতে সর্বোচ্চ পরিমাণ স্যাট নির্ধারণ করতে পারেন। প্রতিটি ভোটের মূল্য একই পরিমাণে রয়েছে তা নিশ্চিত করতে উভয় ক্ষেত্রেই একই পরিমাণ ব্যবহার করুন। যেকোনো পরিমাণ গ্রহণ করতে এটি খালি ছেড়ে দিন।</string>
<string name="error_dialog_zap_error">জ্যাপ পাঠানো যায় নি</string>
<string name="error_dialog_talk_to_user">ব্যবহারকারীকে বার্তা দিন</string>
<string name="error_dialog_button_ok">ঠিক আছে</string>
<string name="relay_information_document_error_assemble_url">%1$s: %2$s পৌঁছাতে ব্যর্থ</string>
<string name="relay_information_document_error_reach_server">%1$s: %2$s পৌঁছাতে ব্যর্থ</string>
<string name="relay_information_document_error_parse_result">%1$s: %2$s থেকে ফলাফল পার্স করা যায় নি</string>
<string name="relay_information_document_error_http_status">%1$s কোড দিয়ে ব্যর্থ হয়েছে %2$s</string>
<string name="active_for">এর জন্য সক্রিয় করুন: </string>
<string name="active_for_home">নীড়</string>
<string name="active_for_msg">বার্তাসমূহ</string>
<string name="active_for_chats">আলাপচারিতা</string>
<string name="active_for_global">বৈশ্বিক</string>
<string name="active_for_search">অনুসন্ধান করুন</string>
<string name="zap_split_title">জ্যাপগুলি ভাগ করুন এবং পাঠান</string>
<string name="zap_split_explainer">সমর্থিত ক্লায়েন্টরা জ্যাপ ভাগ করবে এবং আপনার পরিবর্তে এখানে সংযুক্ত ব্যবহারকারীদেরকে পাঠাবে</string>
<string name="zap_split_serarch_and_add_user">ব্যবহারকারীদের অনুসন্ধান করুন এবং যুক্ত করুন</string>
<string name="zap_split_serarch_and_add_user_placeholder">ইউজারনেম কিংবা প্রদর্শিত নাম দিন</string>
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">%1$s ব্যবহারকারীর স্যাট গ্রহণের উপযোগী কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
<string name="zap_split_weight">শতাংশ</string>
<string name="zap_split_weight_placeholder">২৫</string>
<string name="splitting_zaps_with">এর সাথে জ্যাপ ভাগ করা হচ্ছে</string>
<string name="forwarding_zaps_to">একে জ্যাপ পাঠানো হচ্ছে</string>
<string name="lightning_wallets_not_found2">লাইটনিং ওয়ালেট পাওয়া যায় নি</string>
<string name="paid">পরিশোধিত</string>
<string name="wallet_number">ওয়ালেট %1$s</string>
</resources>

View File

@@ -12,7 +12,6 @@
<string name="could_not_decrypt_the_message">Nepodařilo se dešifrovat zprávu</string>
<string name="group_picture">Obrázek skupiny</string>
<string name="explicit_content">Explicitní obsah</string>
<string name="spam">Spam</string>
<string name="impersonation">Zneužívání identity</string>
<string name="illegal_behavior">Nezákonné jednání</string>
<string name="unknown">Neznámý</string>
@@ -38,8 +37,8 @@
<string name="login_with_a_private_key_to_be_able_to_unfollow">Přihlaste se pomocí soukromého klíče, abyste mohli zrušit sledování</string>
<string name="zaps">Zapy</string>
<string name="view_count">Počet zobrazení</string>
<string name="boost">Boost</string>
<string name="boosted">boostováno</string>
<string name="boost">Zvýšení</string>
<string name="boosted">zvýšeno</string>
<string name="quote">Citovat</string>
<string name="new_amount_in_sats">Nová částka v sats</string>
<string name="add">Přidat</string>
@@ -170,9 +169,6 @@
<string name="nip_05">Nostr Adresa</string>
<string name="never">nikdy</string>
<string name="now">nyní</string>
<string name="h">h</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="nudity">Nahota</string>
<string name="profanity_hateful_speech">Neslušnost / Nenávistná řeč</string>
<string name="report_hateful_speech">Nahlásit nenávistnou řeč</string>
@@ -182,8 +178,7 @@
<string name="mark_all_new_as_read">Označit všechny nové jako přečtené</string>
<string name="mark_all_as_read">Označit všechny jako přečtené</string>
<string name="backup_keys">Zálohovat klíče</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Tipy na zálohování a bezpečnost účtu
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Tipy na zálohování a bezpečnost účtu
\n\nVáš účet je zabezpečen tajným klíčem. Klíč je dlouhý náhodný řetězec začínající **nsec1**. Každý, kdo má přístup k vašemu tajnému klíči, může publikovat obsah pod vaší identitou.
\n\n- **Nepoužívejte** svůj tajný klíč na webových stránkách nebo v softwaru, kterému nedůvěřujete.
\n- Vývojáři aplikace Amethyst vás **nikdy** nebudou žádat o váš tajný klíč.
@@ -193,8 +188,8 @@
<string name="copy_my_secret_key">Zkopírovat můj tajný klíč</string>
<string name="biometric_authentication_failed">Autentizace se nezdařila</string>
<string name="biometric_error">Chyba</string>
<string name="badge_created_by">Vytvořeno uživatelem %1$s</string>
<string name="badge_award_image_for">Obrázek ocenění od %1$s</string>
<string name="badge_created_by">"Vytvořeno uživatelem %1$s"</string>
<string name="badge_award_image_for">"Obrázek ocenění od %1$s"</string>
<string name="new_badge_award_notif">Obdrželi jste nové ocenění</string>
<string name="award_granted_to">Ocenění uděleno uživateli</string>
<string name="copied_note_text_to_clipboard">Text poznámky zkopírován do schránky</string>
@@ -250,7 +245,6 @@
<string name="add_to_public_bookmarks">Přidat do veřejných záložek</string>
<string name="remove_from_private_bookmarks">Odebrat ze soukromých záložek</string>
<string name="remove_from_public_bookmarks">Odebrat z veřejných záložek</string>
<string name="wallet_connect_service">Služba Wallet Connect</string>
<string name="wallet_connect_service_explainer">Autorizuje Nostr Secret k provedení platby zapsům, aniž byste opustili aplikaci. Uchovávejte tajemství v bezpečí a pokud je to možné, používejte soukromý přepojovač.</string>
<string name="wallet_connect_service_pubkey">Veřejný klíč Wallet Connect</string>
@@ -258,7 +252,6 @@
<string name="wallet_connect_service_secret">Tajemství Wallet Connect</string>
<string name="wallet_connect_service_show_secret">Zobrazit tajný klíč</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex soukromý klíč</string>
<string name="pledge_amount_in_sats">Částka záruky v sats</string>
<string name="post_poll">Zveřejnit hlasování</string>
<string name="poll_heading_required">Povinná pole:</string>
@@ -270,99 +263,70 @@
<string name="poll_zap_value_min">Minimální zaps</string>
<string name="poll_zap_value_max">Maximální zaps</string>
<string name="poll_consensus_threshold">Konsensus</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Uzavřít po</string>
<string name="poll_closing_time_days">dnech</string>
<string name="poll_is_closed">Hlasování je uzavřeno pro nové hlasy</string>
<string name="poll_zap_amount">Částka zapsu</string>
<string name="one_vote_per_user_on_atomic_votes">U tohoto typu hlasování je povolen pouze jeden hlas na uživatele</string>
<string name="looking_for_event">"Hledá se událost %1$s"</string>
<string name="custom_zaps_add_a_message">Přidat veřejnou zprávu</string>
<string name="custom_zaps_add_a_message_private">Přidat soukromou zprávu</string>
<string name="custom_zaps_add_a_message_nonzap">Přidat fakturu</string>
<string name="custom_zaps_add_a_message_example">Děkuji za veškerou práci!</string>
<string name="lightning_create_and_add_invoice">Vytvořit a přidat</string>
<string name="poll_author_no_vote">Autoři hlasování nemohou hlasovat ve vlastních hlasováních.</string>
<string name="hash_verification_passed">Obrázek je stejný jako v původním příspěvku</string>
<string name="hash_verification_failed">Obrázek se změnil. Autor možná změnu nezaznamenal</string>
<string name="content_description_add_image">Přidat obrázek</string>
<string name="content_description_add_video">Přidat video</string>
<string name="content_description_add_document">Přidat dokument</string>
<string name="add_content">Přidat do zprávy</string>
<string name="content_description">Popis obsahu</string>
<string name="content_description_example">Modrá loď na bílé písčité pláži při západu slunce</string>
<string name="zap_type">Typ zapsu</string>
<string name="zap_type_explainer">Typ zapsu pro všechny možnosti</string>
<string name="zap_type_public">Veřejný</string>
<string name="zap_type_public_explainer">Všichni mohou vidět transakci a zprávu</string>
<string name="zap_type_private">Soukromý</string>
<string name="zap_type_private_explainer">Odesílatel a příjemce si mohou navzájem vidět a číst zprávu</string>
<string name="zap_type_anonymous">Anonymní</string>
<string name="zap_type_anonymous_explainer">Příjemce a veřejnost neví, kdo platbu poslal</string>
<string name="zap_type_nonzap">Non-Zap</string>
<string name="zap_type_nonzap">Né Zap</string>
<string name="zap_type_nonzap_explainer">Žádná stopa v Nostr, pouze v Lightning</string>
<string name="file_server">Souborový server</string>
<string name="zap_forward_lnAddress">LnAddress nebo @Uživatel</string>
<string name="upload_server_imgur">imgur.com důvěryhodný</string>
<string name="upload_server_imgur_explainer">Imgur může upravit soubor</string>
<string name="upload_server_nostrimg">nostrimg.com důvěryhodný</string>
<string name="upload_server_nostrimg_explainer">NostrImg může upravit soubor</string>
<string name="upload_server_nostrbuild">nostr.build důvěryhodný</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build může upravit soubor</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev důvěryhodný</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev může upravit soubor</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - důvěryhodný</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me může upravit soubor</string>
<string name="upload_server_imgur_nip94">Ověřitelný Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Ověří, zda Imgur upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_nostrimg_nip94">Ověřitelný NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Ověří, zda NostrImg upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_nostrbuild_nip94">Ověřitelný Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Ověří, zda Nostr.build upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_nostrfilesdev_nip94">Ověřitelný Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Ověří, zda Nostrfiles.dev upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_nostrcheckme_nip94">Ověřitelný Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Ověří, zda Nostrcheck.me upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
<string name="connect_via_tor_short">Nastavení Tor/Orbot</string>
<string name="connect_via_tor">Připojit se přes váš Orbot</string>
<string name="do_you_really_want_to_disable_tor_title">Odpojit se od Orbotu/Toru?</string>
<string name="do_you_really_want_to_disable_tor_text">Vaše data budou okamžitě přenesena do běžné sítě</string>
<string name="yes">Ano</string>
<string name="no">Ne</string>
<string name="follow_list_selection">Seznam sledovaných</string>
<string name="follow_list_kind3follows">Všechna sledování</string>
<string name="follow_list_global">Globální</string>
<string name="connect_through_your_orbot_setup_markdown">
## Připojte se přes Tor s Orbotem
<string name="connect_through_your_orbot_setup_markdown"> ## Připojte se přes Tor s Orbotem
\n\n1. Nainstalujte [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Spusťte Orbot
\n3. V Orbotu zkontrolujte port pro Socks. Výchozí hodnota je 9050
@@ -370,54 +334,41 @@
\n5. Nastavte port Socks v této obrazovce
\n6. Stiskněte tlačítko Aktivovat Tor
</string>
<string name="orbot_socks_port">Orbot Socks Port</string>
<string name="invalid_port_number">Neplatné číslo portu</string>
<string name="use_orbot">Použít Orbot</string>
<string name="disconnect_from_your_orbot_setup">Odpojit Tor/Orbot</string>
<string name="app_notification_dms_channel_name">Soukromé zprávy</string>
<string name="app_notification_dms_channel_description">Upozorní vás, když dorazí soukromá zpráva</string>
<string name="app_notification_zaps_channel_name">Přijaté zapsy</string>
<string name="app_notification_zaps_channel_description">Upozorní vás, když vás někdo zapsne</string>
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">Od %1$s</string>
<string name="app_notification_zaps_channel_message_for">pro %1$s</string>
<string name="reply_notify">Upozornit: </string>
<string name="channel_list_join_conversation">Připojit se ke konverzaci</string>
<string name="channel_list_user_or_group_id">ID uživatele nebo skupiny</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent nebo hex</string>
<string name="channel_list_create_channel">Vytvořit</string>
<string name="channel_list_join_channel">Připojit se</string>
<string name="today">Dnes</string>
<string name="content_warning">Upozornění na obsah</string>
<string name="content_warning_explanation">Tento příspěvek obsahuje citlivý obsah, který může být pro některé lidi urážlivý nebo rušivý</string>
<string name="content_warning_hide_all_sensitive_content">Vždy skrýt citlivý obsah</string>
<string name="content_warning_show_all_sensitive_content">Vždy zobrazit citlivý obsah</string>
<string name="content_warning_see_warnings">Vždy zobrazovat upozornění na obsah</string>
<string name="recommended_apps">Doporučuje: </string>
<string name="filter_spam_from_strangers">Filtrovat spam od cizinců</string>
<string name="warn_when_posts_have_reports_from_your_follows">Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob</string>
<string name="new_reaction_symbol">Nový symbol reakce</string>
<string name="no_reaction_type_setup_long_press_to_change">Nebyly vybrány žádné typy reakcí. Dlouhým stiskem změňte</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser">Zap-sběr</string>
<string name="zapraiser_explainer">Přidá cílovou částku sats pro tento příspěvek. Podporované klienty ji mohou zobrazovat jako ukazatel postupu k podněcování darování</string>
<string name="zapraiser_target_amount_in_sats">Cílová částka v sats</string>
<string name="sats_to_complete">Zapraiser na %1$s. Do cíle zbývá %2$s sats</string>
<string name="read_from_relay">Číst z Relay</string>
<string name="write_to_relay">Zapisovat do Relay</string>
<string name="an_error_occurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string>
<string name="owner">Vlastník</string>
<string name="version">Verze</string>
<string name="software">Software</string>
<string name="contact">Kontakt</string>
<string name="supports">Podporované NIPs</string>
<string name="admission_fees">Vstupné</string>
@@ -437,38 +388,29 @@
<string name="minimum_pow">Minimální PoW</string>
<string name="auth">Ověření</string>
<string name="payment">Platba</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Vyměnit</string>
<string name="no_lightning_address_set">Není nastavena žádná Lightning adresa</string>
<string name="copied_token_to_clipboard">Token zkopírován do schránky</string>
<string name="live_stream_live_tag">ŽIVĚ</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">UKONČENO</string>
<string name="live_stream_planned_tag">PLÁNOVÁNO</string>
<string name="live_stream_is_offline">Živé vysílání je offline</string>
<string name="live_stream_has_ended">Živé vysílání ukončeno</string>
<string name="are_you_sure_you_want_to_log_out">Odhlášení vymaže všechny vaše místní informace. Ujistěte se, že máte zálohované své privátní klíče, abyste se vyhnuli ztrátě účtu. Chcete pokračovat?</string>
<string name="followed_tags">Sledované značky</string>
<string name="relay_setup">Rele</string>
<string name="discover_live">Živě</string>
<string name="discover_community">Komunita</string>
<string name="discover_chat">Chaty</string>
<string name="community_approved_posts">Schválené příspěvky</string>
<string name="groups_no_descriptor">Tato skupina nemá popis ani pravidla. Promluvte si s majitelem, aby je přidal/a.</string>
<string name="community_no_descriptor">Tato komunita nemá popis. Promluvte si s majitelem, aby ho přidal/a.</string>
<string name="add_sensitive_content_label">Citlivý obsah</string>
<string name="add_sensitive_content_description">Před zobrazením tohoto obsahu přidá upozornění na citlivý obsah.</string>
<string name="settings">Nastavení</string>
<string name="connectivity_type_always">Vždy</string>
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
<string name="connectivity_type_never">Nikdy</string>
<string name="system">Systém</string>
<string name="light">Světlý</string>
<string name="dark">Tmavý</string>
@@ -479,47 +421,56 @@
<string name="automatically_play_videos">Automaticky přehrávat videa</string>
<string name="automatically_show_url_preview">Automaticky zobrazit náhled URL</string>
<string name="load_image">Načíst obrázek</string>
<string name="spamming_users">Spamovací uživatelé</string>
<string name="muted_button">Ztlumené. Klikněte pro odztlumení</string>
<string name="mute_button">Zvuk zapnutý. Klikněte pro ztlumení</string>
<string name="search_button">Hledat lokální a vzdálené záznamy</string>
<string name="nip05_verified">Adresa Nostr byla ověřena</string>
<string name="nip05_failed">Ověření adresy Nostr se nezdařilo</string>
<string name="nip05_checking">Kontrola adresy Nostr</string>
<string name="select_deselect_all">Vybrat/Zrušit vše</string>
<string name="default_relays">Výchozí</string>
<string name="select_a_relay_to_continue">Vyberte relé pro pokračování</string>
<string name="zap_forward_title">Přeposílat Zapy na:</string>
<string name="zap_forward_explainer">Podporující klienti budou přeposílat Zapy na níže uvedenou LN adresu nebo uživatelský profil místo vaší</string>
<string name="geohash_title">Zveřejnit polohu jako </string>
<string name="geohash_explainer">Přidá Geohash vaší polohy do příspěvku. Veřejnost bude vědět, že se nacházíte do 5 km od aktuální polohy</string>
<string name="add_sensitive_content_explainer">Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující</string>
<string name="new_feature_nip24_might_not_be_available_title">Nová funkce</string>
<string name="new_feature_nip24_might_not_be_available_description">Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-24 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-24 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta.</string>
<string name="new_feature_nip24_activate">Aktivovat</string>
<string name="messages_create_public_chat">Veřejné</string>
<string name="messages_new_message">Soukromé</string>
<string name="messages_new_message_to">Pro</string>
<string name="messages_new_message_subject">Předmět</string>
<string name="messages_new_message_subject_caption">Téma konverzace</string>
<string name="messages_new_message_to_caption">"@Uživatel1, @Uživatel2, @Uživatel3"</string>
<string name="messages_new_message_to_caption">"\@Uživatel1, @Uživatel2, @Uživatel3"</string>
<string name="messages_group_descriptor">Členové této skupiny</string>
<string name="messages_new_subject_message">Vysvětlení členům</string>
<string name="messages_new_subject_message_placeholder">Změna názvu pro nové cíle.</string>
<string name="language_description">Pro rozhraní aplikace</string>
<string name="theme_description">Tmavé, světlé nebo systémové téma</string>
<string name="automatically_load_images_gifs_description">Automaticky načítat obrázky a GIFy</string>
<string name="automatically_play_videos_description">Automaticky přehrávat videa a GIFy</string>
<string name="automatically_show_url_preview_description">Zobrazit náhledy URL</string>
<string name="load_image_description">Kdy načíst obrázek</string>
<string name="copy_url_to_clipboard">Kopírovat URL do schránky</string>
<string name="copy_the_note_id_to_the_clipboard">Kopírovat ID poznámky do schránky</string>
<string name="created_at">Vytvořeno</string>
<string name="rules">Pravidla</string>
<string name="status_update">Aktualizovat svůj stav</string>
<string name="lightning_wallets_not_found">Chyba při zpracování chybové zprávy</string>
<string name="poll_zap_value_min_max_explainer">Hlasy jsou váženy podle hodnoty zapu. Můžete nastavit minimální částku, abyste se vyhnuli spammerům, a maximální částku, abyste zabránili velkým zapperům, kteří by mohli ovládnout hlasování. Použijte stejnou částku v obou polích, aby byla hodnota každého hlasu stejná. Nechte prázdné pro přijetí libovolné částky.</string>
<string name="error_dialog_zap_error">Nelze odeslat zap</string>
<string name="error_dialog_talk_to_user">Poslat zprávu uživateli</string>
<string name="relay_information_document_error_assemble_url">Nepodařilo se dosáhnout %1$s: %2$s</string>
<string name="relay_information_document_error_reach_server">Nepodařilo se dosáhnout %1$s: %2$s</string>
<string name="relay_information_document_error_parse_result">Nepodařilo se zpracovat výsledek z %1$s: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s selhalo s kódem %2$s</string>
<string name="active_for">Aktivní pro: </string>
<string name="active_for_home">Domů</string>
<string name="active_for_msg">Zprávy</string>
<string name="active_for_chats">Chaty</string>
<string name="active_for_global">Celosvětové</string>
<string name="active_for_search">Hledání</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -12,7 +12,6 @@
<string name="could_not_decrypt_the_message">Nachricht konnte nicht entschlüsselt werden</string>
<string name="group_picture">Gruppenbild</string>
<string name="explicit_content">Expliziter Inhalt</string>
<string name="spam">Spam</string>
<string name="impersonation">Vortäuschung</string>
<string name="illegal_behavior">Illegales Verhalten</string>
<string name="unknown">Unbekannt</string>
@@ -23,8 +22,8 @@
<string name="copy_note_id">Notiz-ID kopieren</string>
<string name="broadcast">Senden</string>
<string name="request_deletion">Löschung beantragen</string>
<string name="block_hide_user"><![CDATA[Benutzer blockieren und ausblenden]]></string>
<string name="block_report">Blockieren / Melden</string>
<string name="block_hide_user"><![CDATA[Benutzer blockieren und ausblenden]]></string>
<string name="report_spam_scam">Spam / Betrug melden</string>
<string name="report_impersonation">Vortäuschung melden</string>
<string name="report_explicit_content">Expliziten Inhalt melden</string>
@@ -36,9 +35,8 @@
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Melden Sie sich mit einem privaten Schlüssel an, um Zaps senden zu können</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Melden Sie sich mit einem privaten Schlüssel an, um zu folgen</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Melden Sie sich mit einem privaten Schlüssel an, um nicht mehr zu folgen</string>
<string name="zaps">Zaps</string>
<string name="view_count">Aufrufe</string>
<string name="boost">Boost</string>
<string name="boost">Verstärken</string>
<string name="boosted">Verstärkt</string>
<string name="quote">Zitat</string>
<string name="new_amount_in_sats">Neuer Betrag in Sats</string>
@@ -78,7 +76,6 @@
<string name="failed_to_upload_the_image">Fehler beim Hochladen des Bildes</string>
<string name="relay_address">Relay-Adresse</string>
<string name="posts">Beiträge</string>
<string name="bytes">Bytes</string>
<string name="errors">Fehler</string>
<string name="home_feed">Startseite</string>
<string name="private_message_feed">Private Nachrichten</string>
@@ -165,7 +162,6 @@ anz der Bedingungen ist erforderlich</string>
<string name="public_chat">Öffentlicher Chat</string>
<string name="posts_received">empfangene Beiträge</string>
<string name="remove">Entfernen</string>
<string name="translations_auto">Auto</string>
<string name="translations_translated_from">übersetzt von</string>
<string name="translations_to">zu</string>
<string name="translations_show_in_lang_first">Zuerst in %1$s anzeigen</string>
@@ -174,9 +170,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="nip_05">Nostr-Adresse</string>
<string name="never">nie</string>
<string name="now">jetzt</string>
<string name="h">h</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="h">s</string>
<string name="d">t</string>
<string name="nudity">Nacktheit</string>
<string name="profanity_hateful_speech">Beleidigungen / Hassrede</string>
<string name="report_hateful_speech">Hassrede melden</string>
@@ -186,19 +181,17 @@ anz der Bedingungen ist erforderlich</string>
<string name="mark_all_new_as_read">Alle als neu markieren</string>
<string name="mark_all_as_read">Alle als gelesen markieren</string>
<string name="backup_keys">Schlüssel sichern</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Sicherung und Sicherheitshinweise für Schlüssel
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Sicherung und Sicherheitshinweise für Schlüssel
\n\nIhr Konto ist durch einen geheimen Schlüssel geschützt. Der Schlüssel ist eine lange zufällige Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf Ihren geheimen Schlüssel hat, kann Inhalte unter Verwendung Ihrer Identität veröffentlichen.
\n\n- Geben Sie Ihren geheimen Schlüssel **nicht** auf Websites oder Software ein, denen Sie nicht vertrauen.
\n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen.
\n- Bewahren Sie eine sichere Sicherungskopie Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.
</string>
\n- Bewahren Sie eine sichere Sicherungskopie Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string>
<string name="secret_key_copied_to_clipboard">Geheimer Schlüssel (nsec) in die Zwischenablage kopiert</string>
<string name="copy_my_secret_key">Meinen geheimen Schlüssel kopieren</string>
<string name="biometric_authentication_failed">Authentifizierung fehlgeschlagen</string>
<string name="biometric_error">Fehler</string>
<string name="badge_created_by">Erstellt von %1$s</string>
<string name="badge_award_image_for">Auszeichnungsbild für %1$s</string>
<string name="badge_created_by">"Erstellt von %1$s"</string>
<string name="badge_award_image_for">"Auszeichnungsbild für %1$s"</string>
<string name="new_badge_award_notif">Sie haben eine neue Auszeichnung erhalten</string>
<string name="award_granted_to">Auszeichnung verliehen an</string>
<string name="copied_note_text_to_clipboard">Notiztext in die Zwischenablage kopiert</string>
@@ -236,7 +229,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="report_dialog_impersonation">Bösartige Nachahmung</string>
<string name="report_dialog_nudity">Nacktheit oder explizite Inhalte</string>
<string name="report_dialog_illegal">Illegales Verhalten</string>
<string name="report_dialog_blocking_a_user">Durch das Blockieren eines Benutzers werden deren Inhalte in Ihrer App ausgeblendet. Ihre Notizen sind weiterhin öffentlich sichtbar, auch für Personen, die Sie blockiert haben. Blockierte Benutzer werden auf dem Bildschirm "Sicherheitsfilter" aufgelistet.</string>
<string name="report_dialog_blocking_a_user">Durch das Blockieren eines Benutzers werden deren Inhalte in Ihrer App ausgeblendet. Ihre Notizen sind weiterhin öffentlich sichtbar, auch für Personen, die Sie blockiert haben. Blockierte Benutzer werden auf dem Bildschirm \"Sicherheitsfilter\" aufgelistet.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Benutzer
blockieren & ausblenden]]></string>
@@ -249,7 +242,6 @@ anz der Bedingungen ist erforderlich</string>
<string name="report_dialog_post_report_btn">Meldung veröffentlichen</string>
<string name="report_dialog_title">Blockieren und melden</string>
<string name="block_only">Blockieren</string>
<string name="bookmarks">Lesezeichen</string>
<string name="private_bookmarks">Private Lesezeichen</string>
<string name="public_bookmarks">Öffentliche Lesezeichen</string>
@@ -257,176 +249,129 @@ anz der Bedingungen ist erforderlich</string>
<string name="add_to_public_bookmarks">Zu den öffentlichen Lesezeichen hinzufügen</string>
<string name="remove_from_private_bookmarks">Aus den privaten Lesezeichen entfernen</string>
<string name="remove_from_public_bookmarks">Aus den öffentlichen Lesezeichen entfernen</string>
<string name="wallet_connect_service">Wallet Connect Service</string>
<string name="wallet_connect_service">Wallet-Verbindungsdienst</string>
<string name="wallet_connect_service_explainer">Autorisiert einen Nostr Secret, Zaps ohne Verlassen der App zu bezahlen. Bewahren Sie den geheimen Schlüssel sicher auf und verwenden Sie nach Möglichkeit ein privates Relay</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Public Key</string>
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
<string name="wallet_connect_service_relay">Wallet-Verbindungs-Relay</string>
<string name="wallet_connect_service_secret">Wallet-Verbindungs-Geheimnis</string>
<string name="wallet_connect_service_show_secret">Geheimen Schlüssel anzeigen</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hexadezimaler privater Schlüssel</string>
<string name="pledge_amount_in_sats">Spendenbetrag in Sats</string>
<string name="post_poll">Umfrage veröffentlichen</string>
<string name="poll_heading_required">Erforderliche Felder:</string>
<string name="poll_zap_recipients">Zap-Empfänger</string>
<string name="poll_primary_description">Hauptbeschreibung der Umfrage…</string>
<string name="poll_option_index">Option %s</string>
<string name="poll_option_description">Beschreibung der Umfrageoption</string>
<string name="poll_heading_optional">Optionale Felder:</string>
<string name="poll_zap_value_min">Mindest-Zap-Betrag</string>
<string name="poll_zap_value_max">Maximaler Zap-Betrag</string>
<string name="poll_consensus_threshold">Konsens</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Schließen nach</string>
<string name="poll_closing_time_days">Tagen</string>
<string name="poll_is_closed">Umfrage ist für neue Stimmen geschlossen</string>
<string name="poll_zap_amount">Zap-Betrag</string>
<string name="one_vote_per_user_on_atomic_votes">Es ist nur eine Stimme pro Benutzer für diesen Umfragetyp erlaubt</string>
<string name="looking_for_event">Veranstaltung suchen</string>
<string name="looking_for_event">"Veranstaltung suchen"</string>
<string name="custom_zaps_add_a_message">Füge eine öffentliche Nachricht hinzu</string>
<string name="custom_zaps_add_a_message_private">Füge eine private Nachricht hinzu</string>
<string name="custom_zaps_add_a_message_nonzap">Füge eine Rechnungsnachricht hinzu</string>
<string name="custom_zaps_add_a_message_example">Vielen Dank für deine Arbeit!</string>
<string name="lightning_create_and_add_invoice">Erstellen und Hinzufügen</string>
<string name="poll_author_no_vote">Umfragenautoren können nicht in ihren eigenen Umfragen abstimmen.</string>
<string name="hash_verification_passed">Das Bild ist seitdem Beitrag dasselbe</string>
<string name="hash_verification_failed">Das Bild hat sich geändert. Der Autor hat die Änderung möglicherweise nicht bemerkt.</string>
<string name="content_description_add_image">Bild hinzufügen</string>
<string name="content_description_add_video">Video hinzufügen</string>
<string name="content_description_add_document">Dokument hinzufügen</string>
<string name="add_content">Zur Nachricht hinzufügen</string>
<string name="content_description">Beschreibung des Inhalts</string>
<string name="content_description_example">Ein blaues Boot an einem weißen Sandstrand bei Sonnenuntergang</string>
<string name="zap_type">Zap-Typ</string>
<string name="zap_type_explainer">Zap-Typ für alle Optionen</string>
<string name="zap_type_public">Öffentlich</string>
<string name="zap_type_public_explainer">Jeder kann die Transaktion und die Nachricht sehen</string>
<string name="zap_type_private">Privat</string>
<string name="zap_type_private_explainer">Absender und Empfänger können einander sehen und die Nachricht lesen</string>
<string name="zap_type_anonymous">Anonym</string>
<string name="zap_type_anonymous_explainer">Empfänger und die Öffentlichkeit wissen nicht, wer die Zahlung gesendet hat</string>
<string name="zap_type_nonzap">Non-Zap</string>
<string name="zap_type_nonzap">Keine Zap</string>
<string name="zap_type_nonzap_explainer">Keine Spur in Nostr, nur in Lightning</string>
<string name="file_server">Dateiserver</string>
<string name="zap_forward_lnAddress">LnAddress oder @Benutzer</string>
<string name="upload_server_imgur">imgur.com - vertrauenswürdig</string>
<string name="upload_server_imgur_explainer">Imgur kann die Datei ändern</string>
<string name="upload_server_nostrimg">nostrimg.com - vertrauenswürdig</string>
<string name="upload_server_nostrimg_explainer">NostrImg kann die Datei ändern</string>
<string name="upload_server_nostrbuild">nostr.build - vertrauenswürdig</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build kann die Datei ändern</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - vertrauenswürdig</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kann die Datei ändern</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - vertrauenswürdig</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kann die Datei ändern</string>
<string name="upload_server_imgur_nip94">Verifizierbares Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Überprüft, ob Imgur die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_nostrimg_nip94">Verifizierbares NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Überprüft, ob NostrImg die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_nostrbuild_nip94">Verifizierbares Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Überprüft, ob Nostr.build die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_nostrfilesdev_nip94">Verifizierbares Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Überprüft, ob Nostrfiles.dev die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_nostrcheckme_nip94">Verifizierbares Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Überprüft, ob Nostrcheck.me die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_relays_nip95">Deine Relays (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Dateien werden von deinen Relays gehostet. Neuer NIP: Überprüfe, ob sie unterstützt werden</string>
<string name="connect_via_tor_short">Tor/Orbot-Einrichtung</string>
<string name="connect_via_tor">Verbindung über deine Orbot-Einrichtung herstellen</string>
<string name="do_you_really_want_to_disable_tor_title">Von deiner Orbot/Tor trennen?</string>
<string name="do_you_really_want_to_disable_tor_text">Deine Daten werden sofort im regulären Netzwerk übertragen</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="follow_list_selection">Folgen-Liste</string>
<string name="follow_list_kind3follows">Alle Folgen</string>
<string name="follow_list_global">Global</string>
<string name="connect_through_your_orbot_setup_markdown">
## Über Tor mit Orbot verbinden
<string name="follow_list_global">Weltweit</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Über Tor mit Orbot verbinden
\n\n1. Installiere [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Starte Orbot
\n3. Überprüfe in Orbot den Socks-Port. Standardmäßig wird der Port 9050 verwendet
\n4. Ändere gegebenenfalls den Port in Orbot
\n5. Konfiguriere den Socks-Port in diesem Bildschirm
\n6. Drücke die Aktivieren-Schaltfläche, um Orbot als Proxy zu verwenden
</string>
\n6. Drücke die Aktivieren-Schaltfläche, um Orbot als Proxy zu verwenden</string>
<string name="orbot_socks_port">Orbot Socks-Port</string>
<string name="invalid_port_number">Ungültige Portnummer</string>
<string name="use_orbot">Orbot verwenden</string>
<string name="disconnect_from_your_orbot_setup">Tor/Orbot trennen</string>
<string name="app_notification_dms_channel_name">Private Nachrichten</string>
<string name="app_notification_dms_channel_description">Benachrichtigt dich, wenn eine private Nachricht eingeht</string>
<string name="app_notification_zaps_channel_name">Erhaltene Zaps</string>
<string name="app_notification_zaps_channel_description">Benachrichtigt dich, wenn dich jemand zappt</string>
<string name="app_notification_zaps_channel_message">%1$s Sats</string>
<string name="app_notification_zaps_channel_message_from">Von %1$s</string>
<string name="app_notification_zaps_channel_message_for">für %1$s</string>
<string name="reply_notify">Benachrichtigen: </string>
<string name="channel_list_join_conversation">Unterhaltung beitreten</string>
<string name="channel_list_user_or_group_id">Benutzer- oder Gruppen-ID</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent oder hex</string>
<string name="channel_list_create_channel">Erstellen</string>
<string name="channel_list_join_channel">Beitreten</string>
<string name="today">Heute</string>
<string name="content_warning">Inhaltswarnung</string>
<string name="content_warning_explanation">Dieser Beitrag enthält sensiblen Inhalt, den einige Personen als beleidigend oder verstörend empfinden könnten</string>
<string name="content_warning_hide_all_sensitive_content">Sensiblen Inhalt immer ausblenden</string>
<string name="content_warning_show_all_sensitive_content">Sensiblen Inhalt immer anzeigen</string>
<string name="content_warning_see_warnings">Immer Inhaltswarnungen anzeigen</string>
<string name="recommended_apps">Empfohlene Apps: </string>
<string name="filter_spam_from_strangers">Spam von Fremden filtern</string>
<string name="warn_when_posts_have_reports_from_your_follows">Warnung bei Meldungen von deinen Abonnements</string>
<string name="new_reaction_symbol">Neues Reaktionssymbol</string>
<string name="no_reaction_type_setup_long_press_to_change">Keine Reaktionstypen ausgewählt. Lange drücken, um zu ändern</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser">Zap-Sammlung</string>
<string name="zapraiser_explainer">Fügt einen Zielbetrag in Sats für diesen Beitrag hinzu. Unterstützende Clients können dies als Fortschrittsbalken anzeigen, um Spenden zu fördern</string>
<string name="zapraiser_target_amount_in_sats">Zielbetrag in Sats</string>
<string name="sats_to_complete">Zapraiser bei %1$s. %2$s Sats bis zum Ziel</string>
<string name="read_from_relay">Von Relay lesen</string>
<string name="write_to_relay">In Relay schreiben</string>
<string name="an_error_occurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string>
<string name="owner">Inhaber</string>
<string name="version">Version</string>
<string name="software">Software</string>
<string name="contact">Kontakt</string>
<string name="supports">Unterstützte NIPs</string>
<string name="admission_fees">Eintrittsgebühren</string>
@@ -434,7 +379,6 @@ anz der Bedingungen ist erforderlich</string>
<string name="limitations">Einschränkungen</string>
<string name="countries">Länder</string>
<string name="languages">Sprachen</string>
<string name="tags">Tags</string>
<string name="posting_policy">Veröffentlichungsrichtlinie</string>
<string name="message_length">Nachrichtenlänge</string>
<string name="subscriptions">Abonnements</string>
@@ -450,35 +394,22 @@ anz der Bedingungen ist erforderlich</string>
<string name="cashu_redeem">Einlösen</string>
<string name="no_lightning_address_set">Keine Lightning-Adresse festgelegt</string>
<string name="copied_token_to_clipboard">Token in die Zwischenablage kopiert</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">BEENDET</string>
<string name="live_stream_planned_tag">GEPLANT</string>
<string name="live_stream_is_offline">Livestream ist offline</string>
<string name="live_stream_has_ended">Livestream beendet</string>
<string name="are_you_sure_you_want_to_log_out">Das Abmelden löscht alle Ihre lokalen Informationen. Stellen Sie sicher, dass Sie Ihre privaten Schlüssel gesichert haben, um einen Kontoverlust zu vermeiden. Möchten Sie fortfahren?</string>
<string name="followed_tags">Gefolgte Tags</string>
<string name="relay_setup">Relais</string>
<string name="discover_live">Live</string>
<string name="discover_community">Community</string>
<string name="discover_chat">Chats</string>
<string name="community_approved_posts">Genehmigte Beiträge</string>
<string name="groups_no_descriptor">Diese Gruppe hat keine Beschreibung oder Regeln. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen.</string>
<string name="community_no_descriptor">Diese Community hat keine Beschreibung. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen.</string>
<string name="add_sensitive_content_label">Sensibler Inhalt</string>
<string name="add_sensitive_content_description">Fügt eine Warnung für sensiblen Inhalt hinzu, bevor dieser Inhalt angezeigt wird.</string>
<string name="settings">Einstellungen</string>
<string name="connectivity_type_always">Immer</string>
<string name="connectivity_type_wifi_only">Nur WLAN</string>
<string name="connectivity_type_never">Nie</string>
<string name="system">System</string>
<string name="light">Hell</string>
<string name="dark">Dunkel</string>
<string name="application_preferences">Anwendungseinstellungen</string>
@@ -488,47 +419,54 @@ anz der Bedingungen ist erforderlich</string>
<string name="automatically_play_videos">Videos automatisch abspielen</string>
<string name="automatically_show_url_preview">URL-Vorschau automatisch anzeigen</string>
<string name="load_image">Bild laden</string>
<string name="spamming_users">Spammer</string>
<string name="muted_button">Ton aus. Klicken, um Ton einzuschalten</string>
<string name="mute_button">Ton an. Klicken, um Ton auszuschalten</string>
<string name="search_button">Lokale und entfernte Einträge durchsuchen</string>
<string name="nip05_verified">Nostr-Adresse verifiziert</string>
<string name="nip05_failed">Nostr-Adresse konnte nicht verifiziert werden</string>
<string name="nip05_checking">Nostr-Adresse wird überprüft</string>
<string name="select_deselect_all">Alle auswählen/abwählen</string>
<string name="default_relays">Standard</string>
<string name="select_a_relay_to_continue">Wählen Sie ein Relais aus, um fortzufahren</string>
<string name="zap_forward_title">Weiterleiten von Zaps an:</string>
<string name="zap_forward_explainer">Unterstützende Clients leiten Zaps an die LNAddress oder das Benutzerprofil unten weiter, anstatt an Ihre eigene Adresse</string>
<string name="geohash_title">Ort preisgeben als </string>
<string name="geohash_explainer">Fügt dem Beitrag einen Geohash Ihres Standorts hinzu. Die Öffentlichkeit wird wissen, dass Sie sich innerhalb von 5 km (3 mi) vom aktuellen Standort befinden</string>
<string name="add_sensitive_content_explainer">Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten</string>
<string name="new_feature_nip24_might_not_be_available_title">Neues Feature</string>
<string name="new_feature_nip24_might_not_be_available_description">Um diesen Modus zu aktivieren, muss Amethyst eine NIP-24-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-24 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet.</string>
<string name="new_feature_nip24_activate">Aktivieren</string>
<string name="messages_create_public_chat">Öffentlich</string>
<string name="messages_new_message">Privat</string>
<string name="messages_new_message_to">An</string>
<string name="messages_new_message_subject">Betreff</string>
<string name="messages_new_message_subject_caption">Gesprächsthema</string>
<string name="messages_new_message_to_caption">"@Benutzer1, @Benutzer2, @Benutzer3"</string>
<string name="messages_new_message_to_caption">"\@Benutzer1, @Benutzer2, @Benutzer3"</string>
<string name="messages_group_descriptor">Mitglieder dieser Gruppe</string>
<string name="messages_new_subject_message">Erklärung an Mitglieder</string>
<string name="messages_new_subject_message_placeholder">Ändern des Namens für die neuen Ziele.</string>
<string name="language_description">Für die App-Benutzeroberfläche</string>
<string name="theme_description">Dunkles, helles oder Systemdesign</string>
<string name="automatically_load_images_gifs_description">Bilder und GIFs automatisch laden</string>
<string name="automatically_play_videos_description">Videos und GIFs automatisch abspielen</string>
<string name="automatically_show_url_preview_description">URL-Vorschauen anzeigen</string>
<string name="load_image_description">Wann Bilder geladen werden sollen</string>
</resources>
<string name="copy_url_to_clipboard">URL in die Zwischenablage kopieren</string>
<string name="copy_the_note_id_to_the_clipboard">Notiz-ID in die Zwischenablage kopieren</string>
<string name="created_at">Erstellt am</string>
<string name="rules">Regeln</string>
<string name="status_update">Status aktualisieren</string>
<string name="lightning_wallets_not_found">Fehler beim Verarbeiten der Fehlermeldung</string>
<string name="poll_zap_value_min_max_explainer">Die Abstimmungen werden nach der Höhe des Zaps gewichtet. Sie können einen Mindestbetrag festlegen, um Spam zu verhindern, und einen Höchstbetrag, um zu verhindern, dass große Zapper die Abstimmung dominieren. Verwenden Sie denselben Betrag in beiden Feldern, um sicherzustellen, dass jeder Stimme der gleiche Wert zukommt. Lassen Sie es leer, um jeden Betrag zu akzeptieren.</string>
<string name="error_dialog_zap_error">Zap konnte nicht gesendet werden</string>
<string name="error_dialog_talk_to_user">Mit dem Benutzer kommunizieren</string>
<string name="relay_information_document_error_assemble_url">Konnte %1$s nicht erreichen: %2$s</string>
<string name="relay_information_document_error_reach_server">Konnte %1$s nicht erreichen: %2$s</string>
<string name="relay_information_document_error_parse_result">Konnte das Ergebnis von %1$s nicht verarbeiten: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s fehlgeschlagen mit Code %2$s</string>
<string name="active_for">Aktiv für: </string>
<string name="active_for_home">Startseite</string>
<string name="active_for_msg">Nachrichten</string>
<string name="active_for_search">Suche</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">Montri al la QR-Kodo</string>
<string name="show_qr">Montri QR</string>
<string name="profile_image">Profila Bildo</string>
<string name="scan_qr">Skani QR</string>
<string name="show_anyway">Montri Ĉiukaze</string>
<string name="post_was_flagged_as_inappropriate_by">La afiŝo estis markita kiel netaŭga per</string>
<string name="post_not_found">Afiŝo netrovita</string>
<string name="point_to_the_qr_code">Celu la QR-Kodon</string>
<string name="show_qr">Montri QR</string>
<string name="profile_image">Profila Bildo</string>
<string name="scan_qr">Skani QR</string>
<string name="show_anyway">Montri Ĉiukaze</string>
<string name="post_was_flagged_as_inappropriate_by">La afiŝo estis denuncita kiel netaŭga de</string>
<string name="post_not_found">Afiŝo ŝarĝanta aŭ netrovita en via listo de plusendiloj</string>
<string name="channel_image">Kanala Bildo</string>
<string name="referenced_event_not_found">Referencita evento netrovita</string>
<string name="could_not_decrypt_the_message">Mesaĝo nemalĉifrebla</string>
@@ -21,6 +22,8 @@
<string name="copy_user_pubkey">Kopii ID de Aŭtoro</string>
<string name="copy_note_id">Kopii ID de Noto</string>
<string name="broadcast">Elsendi</string>
<string name="request_deletion">Peti Forigon</string>
<string name="block_report">Bloki / Denunci</string>
<string name="block_hide_user"><![CDATA[Bloki kaj Kaŝi Uzanton]]></string>
<string name="report_spam_scam">Raporti Spamon / Friponon</string>
<string name="report_impersonation">Raporti Personigon</string>
@@ -31,9 +34,11 @@
<string name="login_with_a_private_key_to_like_posts">Ensaluti per sekreta ŝlosilo por ŝati afiŝojn</string>
<string name="no_zap_amount_setup_long_press_to_change">Neniu kvanto da zapoj agordita. Longe premi por ŝanĝigi</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Ensaluti per sekreta ŝlosilo por sendi zapojn</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Ensalutu per sekreta ŝlosilo por sekvi</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Ensalutu per sekreta ŝlosilo por malsekvi</string>
<string name="zaps">Zapoj</string>
<string name="view_count">Nombro da Vidoj</string>
<string name="boost">Boost</string>
<string name="boost">Plifortigi</string>
<string name="boosted">impulsita</string>
<string name="quote">Citi</string>
<string name="new_amount_in_sats">Nova Kvanto da Satoj</string>
@@ -42,6 +47,8 @@
<string name="and">" kaj "</string>
<string name="in_channel">"en kanalo "</string>
<string name="profile_banner">Profila Murpapero</string>
<string name="payment_successful">Sukcesa Pago</string>
<string name="error_parsing_error_message">Eraro analizante de erara mesaĝo</string>
<string name="following">" Sekvanti"</string>
<string name="followers">" Sekvantoj"</string>
<string name="profile">Profilo</string>
@@ -50,17 +57,16 @@
<string name="show_more">Montri Plu</string>
<string name="lightning_invoice">Lightning-Fakturo</string>
<string name="pay">Pagi</string>
<string name="lightning_tips">Lightning-Trinkmono</string>
<string name="lightning_tips">Lightning-Gratifiko</string>
<string name="note_to_receiver">Noto al Ricevanto</string>
<string name="thank_you_so_much">Dankegon!</string>
<string name="amount_in_sats">Kvanto en Satoj</string>
<string name="send_sats">Sendi Satojn</string>
<string name="translations_never_translate_from_lang">"Neniam traduki el "</string>
<string name="error_parsing_preview_for">"Eraro anazilante antaŭrigardon por %1$s : %2$s"</string>
<string name="preview_card_image_for">"Antaŭrigardi Bildon por %1$s"</string>
<string name="error_parsing_preview_for">"Eraro anazilante antaŭrigardon de %1$s : %2$s"</string>
<string name="preview_card_image_for">"Antaŭrigardi Bildon de %1$s"</string>
<string name="new_channel">Nova Kanalo</string>
<string name="channel_name">Kanala Nomo</string>
<string name="my_awesome_group">Mia Mojosega Grupo</string>
<string name="my_awesome_group">Mia Mojosa Grupo</string>
<string name="picture_url">Bilda URL</string>
<string name="description">Priskribo</string>
<string name="about_us">"Pri ni... "</string>
@@ -68,27 +74,28 @@
<string name="post">Afiŝi</string>
<string name="save">Konservi</string>
<string name="create">Krei</string>
<string name="cancel">Malfari</string>
<string name="cancel">Nuligi</string>
<string name="failed_to_upload_the_image">Malsukcesis alŝuti la bildon</string>
<string name="relay_address">Plusendilo Adreso</string>
<string name="posts">Afiŝoj</string>
<string name="bytes">Bajtoj</string>
<string name="errors">Eraroj</string>
<string name="home_feed">Hejma Fluo</string>
<string name="private_message_feed">Privata Mesaĝa Fluo</string>
<string name="public_chat_feed">Publika Babilada Fluo</string>
<string name="private_message_feed">Fluo de Privata Mesaĝoj</string>
<string name="public_chat_feed">Fluo de Publikaj Babiloj</string>
<string name="global_feed">Tutmonda Fluo</string>
<string name="search_feed">Serĉa Fluo</string>
<string name="search_feed">Serĉi Fluon</string>
<string name="add_a_relay">Aldoni Plusendilon</string>
<string name="display_name">Montra Nomo</string>
<string name="my_display_name">Mia montra nomo</string>
<string name="username">Uzantnomo</string>
<string name="my_username">Mia uzantnomo</string>
<string name="about_me">Pri mi</string>
<string name="avatar_url">Avataro URL</string>
<string name="banner_url">Murpapero URL</string>
<string name="website_url">Retejo URL</string>
<string name="avatar_url">URL de Avataro</string>
<string name="banner_url">URL de Murpapero</string>
<string name="website_url">URL de Retejo</string>
<string name="ln_address">LN Adreso</string>
<string name="ln_url_outdated">LN URL (posttempa)</string>
<string name="ln_url_outdated">LN URL (malmoderna)</string>
<string name="image_saved_to_the_gallery">Bildo konservita al la galerio</string>
<string name="failed_to_save_the_image">Malsukcesis konservi la bildon</string>
<string name="upload_image">Alŝuti Bildon</string>
@@ -106,7 +113,7 @@
<string name="conversations">Konversacioj</string>
<string name="notes">Notoj</string>
<string name="replies">Respondoj</string>
<string name="follows">"Sekvoj"</string>
<string name="follows">"Sekvitaj"</string>
<string name="reports">"Raportoj"</string>
<string name="more_options">Pli da Opcioj</string>
<string name="relays">" Plusendiloj"</string>
@@ -119,6 +126,7 @@
<string name="send_a_direct_message">Sendi rektan mesaĝon</string>
<string name="edits_the_user_s_metadata">Redakti metadatumojn de uzanto</string>
<string name="follow">Sekvi</string>
<string name="follow_back">Sekvi reen</string>
<string name="unblock">Malbloki</string>
<string name="copy_user_id">Kopii ID de uzanto</string>
<string name="unblock_user">Malbloki Uzanton</string>
@@ -158,8 +166,8 @@
<string name="translations_to">al</string>
<string name="translations_show_in_lang_first">Montri en %1$s unua</string>
<string name="translations_always_translate_to_lang">Ĉiam traduki al</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="translations_never_translate_from_lang">Neniam traduki el </string>
<string name="nip_05">Adreso de Nostr</string>
<string name="never">neniam</string>
<string name="now">nun</string>
<string name="h">h</string>
@@ -174,8 +182,7 @@
<string name="mark_all_new_as_read">Marki ĉiujn Novajn legitaj</string>
<string name="mark_all_as_read">Marki ĉiujn legitajn</string>
<string name="backup_keys">Sekurkopii Ŝlosilojn</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Sekurkopio de Ŝlosiloj kaj Rekomendoj de Sekureco
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Sekurkopio de Ŝlosiloj kaj Rekomendoj de Sekureco
\n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa hazarda literĉeno komencata kun **nsec1**. Ĉiu, kiu havas aliron al via sekreta ŝlosilo, povas publikigi enhavon per via identeco.
\n\n- **Ne** metu vian sekretan ŝlosilon en ajna retejo aŭ programaro, kiun vi ne fidas.
\n- La programistoj de Amethyst **neniam** petos de vi vian sekretan ŝlosilon.
@@ -193,6 +200,15 @@
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Kopiita @npub de aŭtoro al tondujo</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Kopiita ID de noto (@note1) al tondujo</string>
<string name="select_text_dialog_top">Elekti Tekston</string>
<string name="private_conversation_notification">"&lt;Privata mesaĝo nemalĉifrebla.&gt;\n\nVi estis citita en privata konversacio inter %1$s kaj %2$s."</string>
<string name="account_switch_add_account_dialog_title">Aldoni Novan Konton</string>
<string name="drawer_accounts">Kontoj</string>
<string name="account_switch_select_account">Elekti Konton</string>
<string name="account_switch_add_account_btn">Aldoni Novan Konton</string>
<string name="account_switch_active_account">Aktiva konto</string>
<string name="account_switch_has_private_key">Havas sekretan ŝlosilon</string>
<string name="account_switch_pubkey_only">Nur legebla, neniu sekreta ŝlosilo</string>
<string name="back">Malantaŭen</string>
<string name="quick_action_select">Elekti</string>
<string name="quick_action_share_browser_link">Kunhavigi Ligilon</string>
<string name="quick_action_share">Kunhavigi</string>
@@ -204,6 +220,258 @@
<string name="quick_action_follow">Sekvi</string>
<string name="quick_action_request_deletion_alert_title">Peti Forigiton</string>
<string name="quick_action_request_deletion_alert_body">Amethyst petos, ke via noto estu forigita el la plusendiloj al kiuj vi estas aktuale konektita. Ne estas garantio, ke via noto estos porĉiame forigita el tiuj plusendiloj, aŭ el aliaj plusendiloj, kie ĝi povus esti konservita.</string>
<string name="private_conversation_notification">"&lt;Privata mesaĝo nemalĉifrebla.&gt;\n\nVi estis citita en privata konversacio inter %1$s kaj %2$s."</string>
<string name="quick_action_block_dialog_btn">Bloki</string>
<string name="quick_action_delete_dialog_btn">Forigi</string>
<string name="quick_action_block">Bloki</string>
<string name="quick_action_report">Denunci</string>
<string name="quick_action_delete_button">Forigi</string>
<string name="quick_action_dont_show_again_button">Ne montru denove</string>
<string name="report_dialog_spam">Spamo aŭ trompoj</string>
<string name="report_dialog_profanity">Obscena aŭ malŝata konduto</string>
<string name="report_dialog_impersonation">Malica anstataŭo</string>
<string name="report_dialog_nudity">Nudeco aŭ kruda enhavo</string>
<string name="report_dialog_illegal">Kontraŭleĝa Konduto</string>
<string name="report_dialog_blocking_a_user">Bloki uzanton kaŝus sian enhavon en via apo. Viaj notoj ankoraŭ estos publike videbla, inkluzive de la uzantoj vi blokas. Blokitaj uzantoj estas montritaj en la Sekureca Filtrilo ekrano.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloki kaj Kaŝi Uzanton]]></string>
<string name="report_dialog_report_btn">Denunci</string>
<string name="report_dialog_reminder_public">Afiŝitaj denuncoj estos publike videbla.</string>
<string name="report_dialog_additional_reason_placeholder">Laŭvole doni ekstran kuntekston pri via denunco…</string>
<string name="report_dialog_additional_reason_label">Ekstra Kunteksto</string>
<string name="report_dialog_select_reason_label">Kialo</string>
<string name="report_dialog_select_reason_placeholder">Ekelti kialon…</string>
<string name="report_dialog_post_report_btn">Afiŝi Denuncon</string>
<string name="report_dialog_title">Bloki kaj Denunci</string>
<string name="block_only">Bloki</string>
<string name="bookmarks">Legosignoj</string>
<string name="private_bookmarks">Privataj Legosignoj</string>
<string name="public_bookmarks">Publikaj Legosignoj</string>
<string name="add_to_private_bookmarks">Aldoni al Privataj Legosignoj</string>
<string name="add_to_public_bookmarks">Aldoni al Publikaj Legosignoj</string>
<string name="remove_from_private_bookmarks">Forigi el Privataj Legosignoj</string>
<string name="remove_from_public_bookmarks">Forigi el Publikaj Legosignoj</string>
<string name="wallet_connect_service">Servico de Monujo-Konektado</string>
<string name="wallet_connect_service_explainer">Permesas Nostr-Sekreton por pagi zapojn sen eliri la apon. Konservi la sekreton sekura kaj uzu privatan plusendilon laŭeble</string>
<string name="wallet_connect_service_pubkey">Pubŝlosilo de Wallet Connect</string>
<string name="wallet_connect_service_relay">Plusendilo de Wallet Connect</string>
<string name="wallet_connect_service_secret">Sekreto de Wallet Connect</string>
<string name="wallet_connect_service_show_secret">Montri sekretan ŝlosilon</string>
<string name="wallet_connect_service_secret_placeholder">nsec / heks privata ŝlosilo</string>
<string name="pledge_amount_in_sats">Promesi Kvanton en Satoj</string>
<string name="post_poll">Afiŝi Enketon</string>
<string name="poll_heading_required">Necesaj kampoj:</string>
<string name="poll_zap_recipients">Zapi ricevantojn</string>
<string name="poll_primary_description">Ĉefa priskribo de enketo…</string>
<string name="poll_option_index">Elekto %s</string>
<string name="poll_option_description">Priskribo de elekto de enketo</string>
<string name="poll_heading_optional">Nedevigaj kampoj:</string>
<string name="poll_zap_value_min">Minimumo de Zapoj</string>
<string name="poll_zap_value_max">Maksimumo de Zapoj</string>
<string name="poll_consensus_threshold">Interkonsento</string>
<string name="poll_consensus_threshold_percent">(0-100)%</string>
<string name="poll_closing_time">Fermi post</string>
<string name="poll_closing_time_days">tagoj</string>
<string name="poll_is_closed">Enketo estas fermita al novaj voĉdonoj</string>
<string name="poll_zap_amount">Kvanto de zapoj</string>
<string name="one_vote_per_user_on_atomic_votes">Nur po unu voĉdono por uzanto estas permesita en ĉi tia enketo</string>
<string name="looking_for_event">"Serĉanta Eventon %1$s"</string>
<string name="custom_zaps_add_a_message">Aldoni publikan mesaĝon</string>
<string name="custom_zaps_add_a_message_private">Aldoni privatan mesaĝon</string>
<string name="custom_zaps_add_a_message_nonzap">Aldoni mesaĝon de fakturo</string>
<string name="custom_zaps_add_a_message_example">Dankon pro via laboro!</string>
<string name="lightning_create_and_add_invoice">Krei kaj Aldoni</string>
<string name="poll_author_no_vote">Aŭtoroj de Enketoj ne povas voĉdoni en siajn proprajn enketojn.</string>
<string name="hash_verification_passed">Ĉi tiu enhavo estas la sama ĝis la afiŝo</string>
<string name="hash_verification_failed">Ĉi tiu enhavo ŝanĝiĝis. La aŭtoro eble ne vidis aŭ jesis la ŝanĝo</string>
<string name="content_description_add_image">Aldoni Bildon</string>
<string name="content_description_add_video">Aldoni Videon</string>
<string name="content_description_add_document">Aldoni Dokumenton</string>
<string name="add_content">Aldoni al Mesaĝo</string>
<string name="content_description">Priskribo de enhavoj</string>
<string name="content_description_example">Blua ŝipo en blanka, sabla plaĝo je la sunsubiro</string>
<string name="zap_type">Tipo de Zapo</string>
<string name="zap_type_explainer">Tipo de Zapo por ĉiuj la elektoj</string>
<string name="zap_type_public">Publika</string>
<string name="zap_type_public_explainer">Ĉiu povas vidi la transakcion kaj mesaĝon</string>
<string name="zap_type_private">Privata</string>
<string name="zap_type_private_explainer">Sendinto kaj Ricevanto povas vidi unu la alian kaj legi la mesaĝon</string>
<string name="zap_type_anonymous">Sennoma</string>
<string name="zap_type_anonymous_explainer">Ricevanto kaj la publiko ne scias kiu sendis la pagon</string>
<string name="zap_type_nonzap">Ne-Zapo</string>
<string name="zap_type_nonzap_explainer">Ne spuro en Nostr, nur en Lightning</string>
<string name="file_server">Dosiera Servilo</string>
<string name="zap_forward_lnAddress">LNAdreson aŭ @Uzanto</string>
<string name="upload_server_imgur">imgur.com - fidinda</string>
<string name="upload_server_imgur_explainer">Imgur povas modifi la dosieron</string>
<string name="upload_server_nostrimg">nostrimg.com - fidinda</string>
<string name="upload_server_nostrimg_explainer">NostrImg povas modifi la dosieron</string>
<string name="upload_server_nostrbuild">nostr.build - fidinda</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build povas modifi la dosieron</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - fidinda</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev povas modifi la dosieron</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - fidinda</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me povas modifi la dosieron</string>
<string name="upload_server_imgur_nip94">Atestebla Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Kontrolas se Imgur modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
<string name="upload_server_nostrimg_nip94">Atestebla NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Kontrolas se NostrImg modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
<string name="upload_server_nostrbuild_nip94">Atestebla Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Kontrolas se Nostr.build modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
<string name="upload_server_nostrfilesdev_nip94">Atestebla NostrImg (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Kontrolas se Nostrfiles.dev modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
<string name="upload_server_nostrcheckme_nip94">Atestebla Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Kontrolas se Nostrcheck.me modifis la dosieron. Nova NIP: aliaj klientoj eble ne povas vidi ĝin</string>
<string name="upload_server_relays_nip95">Viaj plusendiloj (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Dosieroj estas konservita de viaj plusendiloj. Nova NIP: kontrolu se ili subtenas</string>
<string name="connect_via_tor_short">Tor/Orbot aranĝo</string>
<string name="connect_via_tor">Konekti tra via Orbot aranĝo</string>
<string name="do_you_really_want_to_disable_tor_title">Malkonekti el via Orbot/Tor?</string>
<string name="do_you_really_want_to_disable_tor_text">Viaj datumoj estos tuj sendita en la normala reto</string>
<string name="yes">Jes</string>
<string name="no">Ne</string>
<string name="follow_list_selection">Listo de Sekvoj</string>
<string name="follow_list_kind3follows">Ĉiuj Sekvoj</string>
<string name="follow_list_global">Ĝenerala</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Konektu tra Tor per Orbot
\n\n1. Instalu [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Aktivigu Orbot
\n3. En Orbot, kontrolu la Socks konektejon. Defaulte uzas 9050
\n4. Se necesa, ŝanĝu la konektejon en Orbot
\n5. Agordu la Socks konektejon en ĉi tiu ekrano
\n6. Alkliki la Aktivigi butonon por uzi Orbot kiel prokuro
</string>
<string name="orbot_socks_port">Socks-Konektejo de Orbot</string>
<string name="invalid_port_number">Nevalida nombro de konektejo</string>
<string name="use_orbot">Uzi Orbot</string>
<string name="disconnect_from_your_orbot_setup">Malkonekti Tor/Orbot</string>
<string name="app_notification_dms_channel_name">Privataj Mesaĝoj</string>
<string name="app_notification_dms_channel_description">Sciigas vin kiam privata mesaĝo alvenas</string>
<string name="app_notification_zaps_channel_name">Zapojn Ricevitaj</string>
<string name="app_notification_zaps_channel_description">Sciigas vin kiam iu zapas vin</string>
<string name="app_notification_zaps_channel_message">%1$s satoj</string>
<string name="app_notification_zaps_channel_message_from">El %1$s</string>
<string name="app_notification_zaps_channel_message_for">pro %1$s</string>
<string name="reply_notify">Sciigi: </string>
<string name="channel_list_join_conversation">Aliĝu al la Konversacio</string>
<string name="channel_list_user_or_group_id">ID de Uzanto aŭ Grupo</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent aŭ hex</string>
<string name="channel_list_create_channel">Krei</string>
<string name="channel_list_join_channel">Aliĝi</string>
<string name="today">Hodiaŭ</string>
<string name="content_warning">Enhava averto</string>
<string name="content_warning_explanation">Ĉi tiu afiŝo enhavas delikatan enhavon, kiu povas ofendi aŭ ĝeni iujn</string>
<string name="content_warning_hide_all_sensitive_content">Ĉiam kaŝi delikatajn enhavojn</string>
<string name="content_warning_show_all_sensitive_content">Ĉiam montri delikatajn enhavojn</string>
<string name="content_warning_see_warnings">Ĉiam montri enhavajn avertojn</string>
<string name="recommended_apps">Rekomendas: </string>
<string name="filter_spam_from_strangers">Filtri spamon el fremduloj</string>
<string name="warn_when_posts_have_reports_from_your_follows">Averti kiam afiŝoj havas denuncojn de viaj sekvituloj</string>
<string name="new_reaction_symbol">Nova Reaga Simbolo</string>
<string name="no_reaction_type_setup_long_press_to_change">Neniaj reagoj selektitaj. Premu longe por ŝanĝi</string>
<string name="zapraiser">Zapkolekto</string>
<string name="zapraiser_explainer">Difinas kvanton de zapoj por kolekti por ĉi tiu afiŝo. Subtenantaj klientoj montrus ĉi tion kiel plenumskalo por kuraĝigi donacojn</string>
<string name="zapraiser_target_amount_in_sats">Celita Kvanto da Satoj</string>
<string name="sats_to_complete">Zapkolekto je %1$s. %2$s satoj al celo</string>
<string name="read_from_relay">Legi el plusendilo</string>
<string name="write_to_relay">Skribi al Plusendilo</string>
<string name="an_error_occurred_trying_to_get_relay_information">Eraro okazis akirante de plusendila informacio el %1$s</string>
<string name="owner">Posedanto</string>
<string name="version">Versio</string>
<string name="software">Programaro</string>
<string name="contact">Kontakto</string>
<string name="supports">Subtenitaj NIPoj</string>
<string name="admission_fees">Enirpago</string>
<string name="payments_url">URL de pagoj</string>
<string name="limitations">Limoj</string>
<string name="countries">Landoj</string>
<string name="languages">Lingvoj</string>
<string name="tags">Etikedoj</string>
<string name="posting_policy">Reguloj de Afiŝado</string>
<string name="message_length">Longo de Mesaĝo</string>
<string name="subscriptions">Abonoj</string>
<string name="filters">Filtriloj</string>
<string name="subscription_id_length">Longo de ID de Abono</string>
<string name="minimum_prefix">Minimuma prefikso</string>
<string name="maximum_event_tags">Maksimumaj etikedoj de evento</string>
<string name="content_length">Longo de enhavo</string>
<string name="minimum_pow">Minimuma PoW</string>
<string name="auth">Auth</string>
<string name="payment">Pago</string>
<string name="cashu">Cashu Signo</string>
<string name="cashu_redeem">Elpagi</string>
<string name="no_lightning_address_set">Neniu Lightning-Adreso agordita</string>
<string name="copied_token_to_clipboard">Signo kopita al tondujo</string>
<string name="live_stream_live_tag">VIVE</string>
<string name="live_stream_offline_tag">SENRETA</string>
<string name="live_stream_ended_tag">FINITA</string>
<string name="live_stream_planned_tag">HORPLANITA</string>
<string name="live_stream_is_offline">Vidfluo estas Senreta</string>
<string name="live_stream_has_ended">Vidfluo Finita</string>
<string name="are_you_sure_you_want_to_log_out">Elsaluti forigas ĉiujn viajn lokajn datumojn. Certigi ke vi sekurkopis vian privatan ŝlosilon por eviti perdi vian konton. Ĉu vi volas daŭrigi?</string>
<string name="followed_tags">Sekvitaj Etikedoj</string>
<string name="relay_setup">Plusendiloj</string>
<string name="discover_live">Vive</string>
<string name="discover_community">Komunumo</string>
<string name="discover_chat">Babilejoj</string>
<string name="community_approved_posts">Aprobitaj Afiŝoj</string>
<string name="groups_no_descriptor">Ĉi tiu grupo ne havas priskribon aŭ regulojn. Parolu kun la posedanto por aldoni</string>
<string name="community_no_descriptor">Ĉi tiu komunumo ne havas priskribon. Parolu kun la posedanto por aldoni</string>
<string name="add_sensitive_content_label">Delikata Enhavo</string>
<string name="add_sensitive_content_description">Aldonas averton de delikata enhavo antaŭ montri ĉi tiun enhavon</string>
<string name="settings">Agordoj</string>
<string name="connectivity_type_always">Ĉiam</string>
<string name="connectivity_type_wifi_only">Nur-Wifi</string>
<string name="connectivity_type_never">Neniam</string>
<string name="system">Sistemo</string>
<string name="light">Hela</string>
<string name="dark">Malhela</string>
<string name="application_preferences">Preferoj de Apo</string>
<string name="language">Lingvo</string>
<string name="theme">Etoso</string>
<string name="automatically_load_images_gifs">Antaŭrigardo de Bildo</string>
<string name="automatically_play_videos">Ludado de Video</string>
<string name="automatically_show_url_preview">Antaŭrigardo de URL</string>
<string name="load_image">Montri bildon</string>
<string name="spamming_users">Spamistoj</string>
<string name="muted_button">Silentigita. Alklaki por malsilentigi</string>
<string name="mute_button">Ŝalta sono. Alklaki por silentigi</string>
<string name="search_button">Serĉi lokajn kaj forajn registrojn</string>
<string name="nip05_verified">Nostr-adreso konfirmita</string>
<string name="nip05_failed">Nostr-adreso malsukcesis konfirmon</string>
<string name="nip05_checking">Kontrolanta de Nostr-adreso</string>
<string name="select_deselect_all">Selekti/Malselekti ĉion</string>
<string name="default_relays">Defaŭlta</string>
<string name="select_a_relay_to_continue">Elekti plusendilon por daŭrigi</string>
<string name="zap_forward_title">Plusendi Zapojn al:</string>
<string name="zap_forward_explainer">Subtenantaj klientoj plusendos zapojn al la ĉi-suba LNAdreso aŭ Uzantprofilo anstataŭ al la via</string>
<string name="geohash_title">Montri Loko kiel </string>
<string name="geohash_explainer">Aldonas Geohash de via loko al la afiŝo. La aŭdienco scios kiam vi estas je 5km (3mi) de la nuna loko</string>
<string name="add_sensitive_content_explainer">Aldonas averton de delikata enhavo antaŭ ol montri vian enhavon. Ĉi tiu estas perfekta por NSFW enhavo aŭ enhavo kiun iu povas trovi ofensiva aŭ ĝena</string>
<string name="new_feature_nip24_might_not_be_available_title">Nova Trajto</string>
<string name="new_feature_nip24_might_not_be_available_description">Por aktivigi ĉi tiun reĝimon Amethyst devus sendi NIP-24 mesaĝon (GiftWrapped, Sealed Direct kaj Grupa Mesaĝojn). NIP-24 estas nova kaj la plejparto de klientoj ankoraŭ ne instalis ĝin. Konfirmu ke la ricevanto uzas kongruan klienton.</string>
<string name="new_feature_nip24_activate">Aktivigi</string>
<string name="messages_create_public_chat">Publika</string>
<string name="messages_new_message">Privata</string>
<string name="messages_new_message_to">Al</string>
<string name="messages_new_message_subject">Temo</string>
<string name="messages_new_message_subject_caption">Temo de la konversacio</string>
<string name="messages_new_message_to_caption">"\@Uzanto1, @Uzanto2, @Uzanto3"</string>
<string name="messages_group_descriptor">Anoj de ĉi tiu grupo</string>
<string name="messages_new_subject_message">Klarigo al anoj</string>
<string name="messages_new_subject_message_placeholder">Ŝanĝante la nomon por la novaj celoj.</string>
<string name="language_description">Pro la Interfaco de la Apo</string>
<string name="theme_description">Malhela, Hela aŭ Sistema Temo</string>
<string name="automatically_load_images_gifs_description">Aŭtomate montri imaĝojn kaj GIFojn</string>
<string name="automatically_play_videos_description">Aŭtomate montri videojn kaj GIFojn</string>
<string name="automatically_show_url_preview_description">Montri antaŭrigardoj de URL</string>
<string name="load_image_description">Kiam montri imaĝojn</string>
<string name="copy_url_to_clipboard">Kopii URL al tondujo</string>
<string name="copy_the_note_id_to_the_clipboard">Kopi ID de Noto al tondujo</string>
<string name="created_at">Kreita en</string>
<string name="rules">Reguloj</string>
<string name="status_update">Ĝistadigi vian staton</string>
<string name="lightning_wallets_not_found">Eraro analizante de erara mesaĝo</string>
<string name="poll_zap_value_min_max_explainer">Voĉdonoj pezas laŭ la zap-kvanto. Vi povas difini minimuman kvanton por eviti spamistojn kaj maksimuman kvanton por eviti ke grandaj zapistoj ekregu la enketon. Uzu la saman kvanton en ambaŭ kampoj por certigi ke ĉiuj voĉdonoj pezus same. Malplenigu ĝin por akcepti ajnan kvanton.</string>
<string name="error_dialog_zap_error">Nekapable sendi zapon</string>
<string name="error_dialog_talk_to_user">Mesaĝi la Uzanto</string>
<string name="error_dialog_button_ok">Okej</string>
</resources>

View File

@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">Apunta al código QR</string>
<string name="show_qr">Mostrar el QR</string>
<string name="profile_image">Imagen de Perfil</string>
<string name="scan_qr">Escanear el QR</string>
<string name="show_anyway">Mostrar de todos modos</string>
<string name="post_was_flagged_as_inappropriate_by">Post marcado como inapropiado por</string>
<string name="channel_image">Imagen del canal</string>
<string name="referenced_event_not_found">Evento referenciado no encontrado</string>
<string name="group_picture">Imagen de grupo</string>
<string name="explicit_content">Contenido explícito</string>
<string name="impersonation">Suplantación de identidad</string>
<string name="illegal_behavior">Comportamiento ilegal</string>
<string name="unknown">Desconocido</string>
<string name="relay_icon">Icono del transmisor</string>
<string name="unknown_author">Autor desconocido</string>
<string name="copy_text">Copiar texto</string>
<string name="copy_user_pubkey">Copiar PubKey del usuario</string>
<string name="copy_note_id">Copiar ID de la nota</string>
<string name="broadcast">Transmisión</string>
<string name="block_hide_user"><![CDATA[Bloquear y ocultar usuario]]></string>
<string name="report_spam_scam">Reportar Spam / Estafa</string>
<string name="report_impersonation">Reportar suplantación de identidad</string>
<string name="report_explicit_content">Reportar contenido explícito</string>
<string name="report_illegal_behaviour">Reportar comportamiento ilegal</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Inicie sesión con una clave privada para poder responder</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Inicie sesión con una clave privada para poder publicar posts</string>
<string name="login_with_a_private_key_to_like_posts">Inicie sesión con una clave privada para dar me gusta a los posts</string>
<string name="no_zap_amount_setup_long_press_to_change">Falta la configuración de Zaps por defecto. Mantén pulsado unos segundos para cambiarla</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Inicie sesión con una clave privada para poder enviar Zaps</string>
<string name="view_count">Total vistas</string>
<string name="boost">Impulsar</string>
<string name="quote">Cita</string>
<string name="new_amount_in_sats">Nueva cantidad en Sats</string>
<string name="add">Añadir</string>
<string name="replying_to">"respondiendo a "</string>
<string name="and">" y "</string>
<string name="in_channel">"en canal "</string>
<string name="profile_banner">Banner de perfil</string>
<string name="following">" Siguiendo"</string>
<string name="followers">" Seguidores"</string>
<string name="profile">Perfil</string>
<string name="security_filters">Filtros de seguridad</string>
<string name="log_out">Cerrar sesión</string>
<string name="show_more">Mostrar más</string>
<string name="lightning_invoice">Factura Lightning</string>
<string name="pay">Pagar</string>
<string name="lightning_tips">Donación Lightning</string>
<string name="note_to_receiver">Nota al receptor</string>
<string name="thank_you_so_much">¡Muchas gracias!</string>
<string name="amount_in_sats">Cantidad en Sats</string>
<string name="send_sats">Enviar Sats</string>
<string name="error_parsing_preview_for">"Error al analizar la vista previa de %1$s : %2$s"</string>
<string name="preview_card_image_for">"Imagen de vista previa para %1$s"</string>
<string name="new_channel">Nuevo canal</string>
<string name="channel_name">Nombre del canal</string>
<string name="my_awesome_group">Mi grupo genial</string>
<string name="picture_url">URL de imagen</string>
<string name="description">Descripción</string>
<string name="about_us">"Sobre nosotros… "</string>
<string name="what_s_on_your_mind">¿Qué tienes en mente?</string>
<string name="post">Enviar</string>
<string name="save">Guardar</string>
<string name="create">Crear</string>
<string name="cancel">Cancelar</string>
<string name="failed_to_upload_the_image">No se pudo cargar la imagen</string>
<string name="relay_address">Dirección del transmisor</string>
<string name="posts">Publicaciones</string>
<string name="errors">Errores</string>
<string name="home_feed">Tus noticias</string>
<string name="private_message_feed">Fuente de mensajes privados</string>
<string name="public_chat_feed">Fuente de chat público</string>
<string name="global_feed">Fuente global</string>
<string name="search_feed">Fuente de búsqueda</string>
<string name="add_a_relay">Anadir transmisor</string>
<string name="display_name">Nombre para mostrar</string>
<string name="my_display_name">Mi nombre para mostrar</string>
<string name="username">Nombre de usuario</string>
<string name="my_username">Mi nombre de usuario</string>
<string name="about_me">Sobre mí</string>
<string name="avatar_url">URL del avatar</string>
<string name="banner_url">URL del banner</string>
<string name="website_url">Dirección web</string>
<string name="ln_address">Dirección LN</string>
<string name="ln_url_outdated">Dirección LN (antiguo)</string>
<string name="image_saved_to_the_gallery">Imagen guardada en la galería</string>
<string name="failed_to_save_the_image">Error al guardar la imagen</string>
<string name="upload_image">Cargar imagen</string>
<string name="uploading">Cargando…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">El usuario no tiene una configuración de dirección Lightning para recibir sats</string>
<string name="reply_here">"responde aquí… "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copia la nota-ID para compartir</string>
<string name="copy_channel_id_note_to_the_clipboard">Copia la nota-ID del canal</string>
<string name="edits_the_channel_metadata">Edita los metadatos del canal</string>
<string name="join">Unirse</string>
<string name="known">Conocidos</string>
<string name="new_requests">Nuevas solicitudes</string>
<string name="blocked_users">Usuarios bloqueados</string>
<string name="new_threads">Temas nuevos</string>
<string name="conversations">Conversaciones</string>
<string name="notes">Notas</string>
<string name="replies">Respuestas</string>
<string name="follows">"Siguiendo"</string>
<string name="reports">"Reportes"</string>
<string name="more_options">Más opciones</string>
<string name="relays">" Retransmisores"</string>
<string name="website">Sitio web</string>
<string name="lightning_address">Dirección Lightning</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copia la ID de Nsec (su contraseña) en el portapapeles para hacer una copia de seguridad</string>
<string name="copy_private_key_to_the_clipboard">Copiar clave privada al portapapeles</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copia la clave pública al portapapeles para compartir</string>
<string name="copy_public_key_npub_to_the_clipboard">Copie la clave pública (NPub) al portapapeles</string>
<string name="send_a_direct_message">Enviar un mensaje directo</string>
<string name="edits_the_user_s_metadata">Edita los metadatos del usuario</string>
<string name="follow">Seguir</string>
<string name="unblock">Desbloquear</string>
<string name="copy_user_id">Copiar ID de usuario</string>
<string name="unblock_user">Desbloquear usuario</string>
<string name="npub_hex_username">"npub, hex, username "</string>
<string name="clear">Limpiar</string>
<string name="app_logo">Logo de la app</string>
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string>
<string name="show_password">Mostrar contraseña</string>
<string name="hide_password">Ocultar contraseña</string>
<string name="invalid_key">Clave invalida</string>
<string name="i_accept_the">"Acepto los "</string>
<string name="terms_of_use">términos de uso</string>
<string name="acceptance_of_terms_is_required">Se requiere la aceptación de los términos</string>
<string name="key_is_required">Se requiere la clave</string>
<string name="login">Acceso</string>
<string name="generate_a_new_key">Generar una nueva clave</string>
<string name="loading_feed">Cargando el tablón…</string>
<string name="error_loading_replies">"Error al cargar las respuestas: "</string>
<string name="try_again">Intentar otra vez</string>
<string name="feed_is_empty">Tablón vacío</string>
<string name="refresh">Actualizar</string>
<string name="created">Creado</string>
<string name="with_description_of">con descripción de</string>
<string name="and_picture">y foto</string>
<string name="changed_chat_name_to">cambió el nombre del chat a</string>
<string name="description_to">descripción a</string>
<string name="and_picture_to">e imagen a</string>
<string name="leave">Dejar</string>
<string name="unfollow">Dejar de seguir</string>
<string name="channel_created">Canal creado</string>
<string name="channel_information_changed_to">"La información del canal cambió a"</string>
<string name="public_chat">Chat público</string>
<string name="posts_received">publicaciones recibidas</string>
<string name="remove">Eliminar</string>
<string name="translations_translated_from">traducido de</string>
<string name="translations_to">a</string>
<string name="translations_show_in_lang_first">Mostrar en %1$s primero</string>
<string name="translations_always_translate_to_lang">Traducir siempre a %1$s</string>
<string name="translations_never_translate_from_lang">Nunca traducir desde %1$s</string>
<string name="never">nunca</string>
<string name="now">ahora</string>
<string name="nudity">Desnudos</string>
<string name="profanity_hateful_speech">Blasfemias / Discurso de odio</string>
<string name="report_hateful_speech">Denuncia discurso de odio</string>
<string name="report_nudity_porn">Reportar desnudos / Porno</string>
<string name="others">otros</string>
<string name="mark_all_known_as_read">Marcar los conocidos como leídos</string>
<string name="mark_all_new_as_read">Marcar los nuevos como leídos</string>
<string name="mark_all_as_read">Marcar todos como leídos</string>
<string name="backup_keys">Claves de respaldo</string>
<string name="secret_key_copied_to_clipboard">Clave secreta (nsec) copiada al portapapeles</string>
<string name="copy_my_secret_key">Copiar mi clave secreta</string>
<string name="biometric_authentication_failed">Autenticación fallida</string>
<string name="badge_created_by">"Credo por %1$s"</string>
<string name="badge_award_image_for">"Imagen del Badge para %1$s"</string>
<string name="new_badge_award_notif">Has recibido un nuevo Badge</string>
<string name="award_granted_to">Badge otorgado a</string>
<string name="copied_note_text_to_clipboard">Texto de nota copiado al portapapeles</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">\@npub del autor copiado al portapapeles</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">ID de nota copiada (@note1) al portapapeles</string>
<string name="select_text_dialog_top">Seleccionar texto</string>
<string name="quick_action_select">Selecciona</string>
<string name="quick_action_share_browser_link">Compartir enlace del navegador</string>
<string name="quick_action_share">Compartir</string>
<string name="quick_action_copy_user_id">ID del autor</string>
<string name="quick_action_copy_note_id">ID de la nota</string>
<string name="quick_action_copy_text">Copiar texto</string>
<string name="quick_action_delete">Eliminar</string>
<string name="quick_action_unfollow">Dejar de seguir</string>
<string name="quick_action_follow">Seguir</string>
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse.</string>
</resources>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">Apunta al código QR</string>
<string name="profile_image">Foto de perfil</string>
<string name="scan_qr">Escanear el código QR</string>
<string name="show_anyway">Mostrar de todos modos</string>
<string name="post_was_flagged_as_inappropriate_by">La publicación fue reportada por</string>
<string name="channel_image">Imagen del canal</string>
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
<string name="group_picture">Foto de grupo</string>
<string name="explicit_content">Contenido explícito</string>
<string name="spam">Spam</string>
<string name="unknown">Desconocido</string>
<string name="unknown_author">Autor desconocido</string>
<string name="copy_text">Copiar texto</string>
<string name="copy_user_pubkey">Copiar llave pública del usuario</string>
<string name="block_report">Bloquear / reportar</string>
<string name="block_hide_user"><![CDATA[Bloquear y ocultar usuario]]></string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name_release" translatable="false">Amethyst</string>
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
<string name="point_to_the_qr_code"> به QRکد اشاره کنید</string>
<string name="show_qr">QR کد را نشان بده </string>
<string name="profile_image">تصویر نمایه</string>
@@ -23,8 +22,9 @@
<string name="copy_user_pubkey">کپی شناسه نویسنده</string>
<string name="copy_note_id">کپی شناسه یادداشت</string>
<string name="broadcast">انتشار</string>
<string name="request_deletion">درخواست حذف/string></string>
<string name="block_hide_user">بلاک/پنهان کردن کاربر</string>
<string name="request_deletion">درخواست حذف/string&gt;</string>
<string name="block_report">مسدود/گزارش</string>
<string name="block_hide_user"><![CDATA[بلاک/پنهان کردن کاربر]]></string>
<string name="report_spam_scam">گزارش اسپم/کلاهبرداری</string>
<string name="report_impersonation">گزارش جعل هویت</string>
<string name="report_explicit_content">گزارش محتوای نامناسب</string>
@@ -34,6 +34,8 @@
<string name="login_with_a_private_key_to_like_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را لایک کنید</string>
<string name="no_zap_amount_setup_long_press_to_change">مقداری برای زپ تعیین نشده. نگه دارید تا تغییر دهید</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">با کلید خصوصی وارد شوید تا بتوانید زپ کنید</string>
<string name="login_with_a_private_key_to_be_able_to_follow">با یک کلید خصوصی وارد شوید تا بتوانید دنبال کنید</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">با یک کلید خصوصی وارد شوید تا بتوانید دنبال کردن را لغو کنید</string>
<string name="zaps">زپ</string>
<string name="view_count">دیدن تعداد</string>
<string name="boost">بازنشر</string>
@@ -45,6 +47,8 @@
<string name="and">" و "</string>
<string name="in_channel">"در کانال "</string>
<string name="profile_banner">بنر پروفایل</string>
<string name="payment_successful">پرداخت موفق</string>
<string name="error_parsing_error_message">خطا در تفسیر پیغام خطا</string>
<string name="following">"دنبال شونده"</string>
<string name="followers">"دنبال کننده"</string>
<string name="profile">پروفایل</string>
@@ -143,7 +147,7 @@
<string name="error_loading_replies">" :خطا در بارگیری پاسخ ها"</string>
<string name="try_again">دوباره تلاش کنید</string>
<string name="feed_is_empty">خبرنامه خالیست</string>
<string name="refresh">تازه سازی</string>
<string name="refresh">تازهسازی</string>
<string name="created">ساخته شد</string>
<string name="with_description_of">با توصیف</string>
<string name="and_picture">و تصویر</string>
@@ -157,20 +161,18 @@
<string name="public_chat">گفتگوی عمومی</string>
<string name="posts_received">یادداشت های دریافت شده</string>
<string name="remove">بیرون کردن</string>
<string name="sats" translatable="false">ساتوشی</string>
<string name="translations_auto">خودکار</string>
<string name="translations_translated_from">ترجمه از</string>
<string name="translations_to">به</string>
<string name="translations_show_in_lang_first">ابتدا به نشان بده %1$s</string>
<string name="translations_always_translate_to_lang">همیشه به ترجمه کن %1$s</string>
<string name="translations_never_translate_from_lang">هرگز به ترجمه نکن %1$s</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="nip_05">آدرس ناستر</string>
<string name="never">هرگز</string>
<string name="now">اکنون</string>
<string name="h">h</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="h">ساعت</string>
<string name="m">دقیقه</string>
<string name="d">روز</string>
<string name="nudity">برهنگی</string>
<string name="profanity_hateful_speech">ناسزا / نفرت پراکنی</string>
<string name="report_hateful_speech">گزارش نفرت پراکنی</string>
@@ -180,8 +182,7 @@
<string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string>
<string name="mark_all_as_read">همه را خوانده شده کن</string>
<string name="backup_keys">بازیابی کلیدها</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## راهنمای ایمنی و بازیابی کلید
<string name="account_backup_tips_md" tools:ignore="Typos"> ## راهنمای ایمنی و بازیابی کلید
\n\n- حساب کاربری شما با یک کلید خصوصی ایمن می شود. این کلید یک رشته تصادفی اعداد و ارقام است که با **nsec1** آغاز می گردد.
\n\n- هر کس به کلید خصوصی شما دست یابد می تواند با هویت شما اقدام به انتشار محتوا نماید. کلید خصوصی خود را در هیچ نرم افزار یا وب سایتی که به آن اعتماد ندارید قرار ندهید.
\n- سازندگان آماتیست **هرگز** از شما کلید خصوصی تان را نمی خواهند.
@@ -196,17 +197,9 @@
<string name="new_badge_award_notif">یک مدال پاداش دریافت کردید</string>
<string name="award_granted_to">مدال پاداش داده شد به</string>
<string name="copied_note_text_to_clipboard">یادداشت در کلیپبورد کپی شد</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">@npubنویسنده در کلیپبورد کپی شد </string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">\@npubنویسنده در کلیپبورد کپی شد </string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">(@note1)شناسه یادداشت در کلیپبورد کپی شد</string>
<string name="select_text_dialog_top">انتخاب متن</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;نمی توان پیام خصوصی را رمزگشایی کردن&gt;\n\nYou در پیام خصوصی/رمزگذاری شده بین و به شما اشاره شده %1$s %2$s."</string>
<string name="account_switch_add_account_dialog_title">افزودن حساب کاربری جدید</string>
<string name="drawer_accounts">حساب های کاربری</string>
@@ -225,7 +218,7 @@
<string name="quick_action_delete">حذف</string>
<string name="quick_action_unfollow">دنبال نکردن</string>
<string name="quick_action_follow">دنبال کردن</string>
<string name="quick_action_request_deletion_alert_title">Request Deletion</string>
<string name="quick_action_request_deletion_alert_title">درخواست حذف</string>
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
<string name="quick_action_block_dialog_btn">بلاک</string>
<string name="quick_action_delete_dialog_btn">حذف</string>
@@ -239,7 +232,7 @@
<string name="report_dialog_nudity">محتوای برهنگی یا نمایان</string>
<string name="report_dialog_illegal">رفتار ناهنجار</string>
<string name="report_dialog_blocking_a_user">بلاک کردن کاربر محتوای ایشان را در اپ شما پنهان می کند. یادداشت های شما همچنان بطور عمومی دیده می شوند که شامل کاربران بلاک شده توسط شما نیز هست. کاربران بلاک شده در صفحه فیلترهای ایمنی فهرست می شوند.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Block & Hide User]]></string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[مسدود و پنهان کردن کاربر]]></string>
<string name="report_dialog_report_btn">گزارش سواستفاده</string>
<string name="report_dialog_reminder_public">تمام گزارش ها بطور عمومی دیده می شوند</string>
<string name="report_dialog_additional_reason_placeholder">در صورت تمایل توضیحاتی به گزارش خود بیفزایید...</string>
@@ -249,7 +242,6 @@
<string name="report_dialog_post_report_btn">گزارش یادداشت</string>
<string name="report_dialog_title">بلاک و گزارش</string>
<string name="block_only">بلاک</string>
<string name="bookmarks">علامت گذاشته ها</string>
<string name="private_bookmarks">علامت گاشته های خصوصی</string>
<string name="public_bookmarks">علامت گذاشته های عمومی</string>
@@ -257,7 +249,6 @@
<string name="add_to_public_bookmarks">افزودن به علامت گذاشته های عمومی</string>
<string name="remove_from_private_bookmarks">برداشتن از علامت گذاشته های خصوصی</string>
<string name="remove_from_public_bookmarks">برداشتن از علامت گذاشته های خصوصی</string>
<string name="wallet_connect_service">خدمات اتصال به کیف پول</string>
<string name="wallet_connect_service_explainer">اجازه رمز نوستر برای پرداخت زپ بدون خارج شدن از اپ. رمز را ایمن نگه دارید و در صورت امکان از رله خصوصی استفاده کنید.</string>
<string name="wallet_connect_service_pubkey">کلید عمومی اتصال به کیف پول</string>
@@ -265,7 +256,6 @@
<string name="wallet_connect_service_secret">رمز اتصال به کیف پول</string>
<string name="wallet_connect_service_show_secret">نمایش کلید خصوصی</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex کلید خصوصی</string>
<string name="pledge_amount_in_sats">مبلغ وثیقه به ساتوشی</string>
<string name="post_poll">ارسال نظرسنجی</string>
<string name="poll_heading_required">قسمت های الزامی:</string>
@@ -283,92 +273,65 @@
<string name="poll_is_closed">نظر سنجی برای رای جدید بسته است</string>
<string name="poll_zap_amount">مبلغ زپ</string>
<string name="one_vote_per_user_on_atomic_votes">فقط یک رای به ازای هر کاربر در این نوع نظرسنجی مجاز است</string>
<string name="looking_for_event">"جستجوی رویداد %1$s"</string>
<string name="custom_zaps_add_a_message">افزودن پیام عمومی</string>
<string name="custom_zaps_add_a_message_private">افزودن پیام خصوصی</string>
<string name="custom_zaps_add_a_message_nonzap">افزودن پیام صورتحساب</string>
<string name="custom_zaps_add_a_message_example">از زحمات شما متشکریم!</string>
<string name="lightning_create_and_add_invoice">ساختن و افزودن</string>
<string name="poll_author_no_vote">سازندگان نظرسنجی نمی توانند در نظر سنجی خود رای دهند.</string>
<string name="poll_hashtag" translatable="false">#زپ_نظرسنجی</string>
<string name="hash_verification_passed">تصویر از زمان ارسال یکسان است</string>
<string name="hash_verification_failed">تصویر تغییر کرده است. ممکن است نویسنده تغییر را ندیده باشد.</string>
<string name="content_description_add_image">افزودن تصویر</string>
<string name="content_description_add_video">افزودن فیلم</string>
<string name="content_description_add_document">افزودن مدارک</string>
<string name="add_content">افزودن به پیام</string>
<string name="content_description">توصیف محتوا</string>
<string name="content_description_example">قایقی آبی در ساحل شنی سفید هنگام غروب</string>
<string name="zap_type">نوع زپ</string>
<string name="zap_type_explainer">نوع زپ برای همه گزینه ها</string>
<string name="zap_type_public">عمومی</string>
<string name="zap_type_public_explainer">همه می توانند تراکنش و پیام را ببینند</string>
<string name="zap_type_private">خصوصی</string>
<string name="zap_type_private_explainer">فرستنده و گیرنده می توانند یکدیگر و پیام را ببینند</string>
<string name="zap_type_anonymous">ناشناس</string>
<string name="zap_type_anonymous_explainer">گیرنده و دیگران نمی دانند چه کسی پرداخت کرده است</string>
<string name="zap_type_nonzap">غیرزپ</string>
<string name="zap_type_nonzap_explainer">هیچ ردی در نوستر نمی ماند، فقط در شبکه لایتنینگ انجام می شود</string>
<string name="file_server">سرور فایل</string>
<string name="zap_forward_lnAddress">آدرس لایتنینگ یا @User</string>
<string name="upload_server_imgur">imgur.com - معتمد</string>
<string name="upload_server_imgur_explainer">Imgur می تواند فایل را بهینه کند</string>
<string name="upload_server_nostrimg">nostrimg.com - معتمد</string>
<string name="upload_server_nostrimg_explainer">NostrImg می تواند فایل را بهینه کند</string>
<string name="upload_server_nostrbuild">nostr.build - معتمد</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build می تواند فایل را بهینه کند</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - معتمد</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev می تواند فایل را بهینه کند</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - معتمد</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me می تواند فایل را اصلاح کند</string>
<string name="upload_server_imgur_nip94">قابل بررسی Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">بررسی کنید که Imgur فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند. </string>
<string name="upload_server_nostrimg_nip94">قابل بررسی NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">بررسی کنید که NostrImg فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94">Nostr.build (NIP-94) قابل تایید</string>
<string name="upload_server_nostrbuild_nip94_explainer">بررسی کنید که Nostr.build فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
<string name="upload_server_nostrfilesdev_nip94">قابل بررسی Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">بررسی کنید که Nostrfiles.dev فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me (NIP-94) قابل تایید</string>
<string name="upload_server_nostrcheckme_nip94_explainer">بررسی می کند آیا Nostrcheck.me فایل را اصلاح کرده است. NIP جدید: سایر کلاینت ها ممکن است این را نبینند</string>
<string name="upload_server_relays_nip95">رله های شما (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">فایل ها در رله های شما میزبانی می شوند. NIPجدید: بررسی کنید آیا پشتیبانی می کنند یا خیر </string>
<string name="connect_via_tor_short">Tor/Orbot تنظیمات</string>
<string name="connect_via_tor"> از طریق تنظیم Orbot متصل شوید</string>
<string name="do_you_really_want_to_disable_tor_title">قطع اتصال از Orbot/Tor?</string>
<string name="do_you_really_want_to_disable_tor_text">داده های شما بلافاصله در شبکه معمولی منتقل می شوند</string>
<string name="yes">بله</string>
<string name="no">خیر</string>
<string name="follow_list_selection">لیست دنبال ها </string>
<string name="follow_list_selection">لیست دنبال ها</string>
<string name="follow_list_kind3follows">همه دنبال ها</string>
<string name="follow_list_global">همگانی</string>
<string name="connect_through_your_orbot_setup_markdown">
## از طریق Tor با Orbot متصل شوید
<string name="connect_through_your_orbot_setup_markdown"> ## از طریق Tor با Orbot متصل شوید
\n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. شروع Orbot
\n3. در Orbot, پورت Socksرا علامت بزنید.مقدار پیش فرض 9050 است.
@@ -376,38 +339,139 @@
\n5. پورت Socks را در این صفحه پیکربندی کنید
\n6. دکمه Activate را بفشارید تا از Orbot به عنوان پراکسی استفده نمایید
</string>
<string name="orbot_socks_port">Orbot Socks Port</string>
<string name="orbot_socks_port">درگاه Orbot Socks</string>
<string name="invalid_port_number">شماره پورت نامعتبر</string>
<string name="use_orbot">استفاده از Orbot</string>
<string name="disconnect_from_your_orbot_setup">قطع اتصال Tor/Orbot</string>
<string name="app_notification_channel_id" translatable="false">DefaultChannelID</string>
<string name="app_notification_private_message" translatable="false">اعلان جدید</string>
<string name="app_notification_dms_channel_id" translatable="false">PrivateMessagesID</string>
<string name="app_notification_dms_channel_name">پیام خصوصی</string>
<string name="app_notification_dms_channel_description">وقتی پیام خصوصی دریافت شود اعلان می کند</string>
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
<string name="app_notification_zaps_channel_name">زپ دریافت شد</string>
<string name="app_notification_zaps_channel_name">زپ دریافت شده</string>
<string name="app_notification_zaps_channel_description">وقتی کسی زپ کند اعلام می کند</string>
<string name="app_notification_zaps_channel_message">%1$s ساتوشی</string>
<string name="app_notification_zaps_channel_message_from">از %1$s</string>
<string name="app_notification_zaps_channel_message_for">برای %1$s</string>
<string name="reply_notify">اعلان: </string>
<string name="channel_list_join_conversation">پیوستن به گفتگو</string>
<string name="channel_list_user_or_group_id">شناسه گروه یا کاربر</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent or hex</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent یا hex</string>
<string name="channel_list_create_channel">ساختن</string>
<string name="channel_list_join_channel">پیوستن</string>
<string name="today">امروز</string>
<string name="content_warning">هشدار محتوا</string>
<string name="content_warning_explanation">این یادداشت حاوی محتوای حساس است که ممکن است از نظر برخی توهین آمیز یا مشوش کننده باشد</string>
<string name="content_warning_hide_all_sensitive_content">همیشه محتوای حساس را پنهان کن</string>
<string name="content_warning_show_all_sensitive_content">همیشه محتوای حساس را نشان بده</string>
<string name="content_warning_see_warnings">همیشه هشدار محتوا را نشان بده</string>
<string name="recommended_apps">توصیه ها: </string>
<string name="filter_spam_from_strangers">اسپم غریبه ها را فیلتر کن</string>
<string name="warn_when_posts_have_reports_from_your_follows">اگر یادداشت توسط افرادی که فالو می کنید گزارش شده هشدار بده</string>
<string name="new_reaction_symbol">نماد جدید واکنش</string>
<string name="no_reaction_type_setup_long_press_to_change">هیچ نوع واکنشی انتخاب نشده. نگه دارید تا تغییر دهید</string>
<string name="zapraiser">زپ گیری</string>
<string name="zapraiser_explainer">یک مبلغ ساتوشی هدف به این یادداشت بیافزایید. کلاینت های سازگار ممکن احتمالا این ویژگی را به شکل نوار پیش رونده نشان می دهند تا مشوق کمک مالی باشد.</string>
<string name="zapraiser_target_amount_in_sats">مبلغ هدف به ساتوشی</string>
<string name="sats_to_complete">زپ گیری تا %1$s. %2$s مانده تا هدف</string>
<string name="read_from_relay">خواندن از رله</string>
<string name="write_to_relay">نوشتن به رله</string>
<string name="an_error_occurred_trying_to_get_relay_information">در تلاش برای گرفتن اطلاعات رله از %1$s خطایی رخ داد</string>
<string name="owner">مالک</string>
<string name="version">نگارش</string>
<string name="software">نرم‌افزار</string>
<string name="contact">مخاطب</string>
<string name="supports">NIP های پشتیبانی شده</string>
<string name="admission_fees">کارمزد تراکنش</string>
<string name="payments_url">Url پرداخت ها</string>
<string name="limitations">محدودیت ها</string>
<string name="countries">کشورها</string>
<string name="languages">زبان‌ها</string>
<string name="tags">برچسب‌ها</string>
<string name="posting_policy">سیاست ارسال محتوا</string>
<string name="message_length">طول پیام</string>
<string name="subscriptions">اشتراک</string>
<string name="filters">فیلترها</string>
<string name="subscription_id_length">طول شناسه اشتراک</string>
<string name="minimum_prefix">حداقل پیشوند</string>
<string name="maximum_event_tags">حداکثر تعداد برچسب رویداد</string>
<string name="content_length">طول محتوا</string>
<string name="minimum_pow">حداقل PoW</string>
<string name="auth">Auth</string>
<string name="payment">پرداخت</string>
<string name="cashu">توکن Cashu</string>
<string name="cashu_redeem">بازپرداخت</string>
<string name="no_lightning_address_set">آدرس لایتنینگ تنظیم نشده است</string>
<string name="copied_token_to_clipboard">توکن به کلیپبورد کپی شد</string>
<string name="live_stream_live_tag">زنده</string>
<string name="live_stream_offline_tag">آفلاین</string>
<string name="live_stream_ended_tag">تمام شد</string>
<string name="live_stream_planned_tag">زمان بندی شده</string>
<string name="live_stream_is_offline">پخش زنده آفلاین است</string>
<string name="live_stream_has_ended">پخش زنده تمام شد</string>
<string name="are_you_sure_you_want_to_log_out">خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟</string>
<string name="followed_tags">برچسب های دنبال شده</string>
<string name="relay_setup">رله ها</string>
<string name="discover_live">زنده</string>
<string name="discover_community">انجمن</string>
<string name="discover_chat">گپ</string>
<string name="community_approved_posts">یادداشت های تایید شده</string>
<string name="groups_no_descriptor">این گروه هیچ توصیف و قوانینی ندارد. با مالک گروه برای افزودن آن صحبت کنید.</string>
<string name="community_no_descriptor">این انجمن هیچ توضیح و قوانینی ندارد. با مالک گروه برای افزودن آن صحبت کنید.</string>
<string name="add_sensitive_content_label">محتوای حساس</string>
<string name="add_sensitive_content_description">هشدار محتوای حساس پیش از نمایش محتوا اضافه می کند</string>
<string name="settings">تنظیمات</string>
<string name="connectivity_type_always">همیشه</string>
<string name="connectivity_type_wifi_only">فقط Wifi</string>
<string name="connectivity_type_never">هرگز</string>
<string name="system">سیستم</string>
<string name="light">روشن</string>
<string name="dark">تاریک</string>
<string name="application_preferences">ترجیحات اپلیکیشن</string>
<string name="language">زبان</string>
<string name="theme">پوسته</string>
<string name="automatically_load_images_gifs">پیش نمایش تصویر</string>
<string name="automatically_play_videos">پخش ویدئو</string>
<string name="automatically_show_url_preview">پیش نمایش URL</string>
<string name="load_image">بارگیری تصویر</string>
<string name="spamming_users">اسپمر</string>
<string name="muted_button">بیصدا شده. برای لغو کلیک کنید</string>
<string name="mute_button">باصدا. برای بیصدا کردن کلیک کنید</string>
<string name="search_button">سوابق محلی و دور را بگرد</string>
<string name="nip05_verified">آدرس ناستر تایید شده بود</string>
<string name="nip05_failed">آدرس ناستر تایید نشد</string>
<string name="nip05_checking">بررسی آدرس ناستر</string>
<string name="select_deselect_all">انتخاب یا لغو انتخاب همه</string>
<string name="default_relays">پیش‌فرض</string>
<string name="select_a_relay_to_continue">برای ادامه یک رله انتخاب کنید</string>
<string name="zap_forward_title">باز ارسال زپ ها به:</string>
<string name="zap_forward_explainer">کلاینت هایی که این قابلیت را دارند زپ ها را به جای شما به LNAddress یا نمایه کاربر زیر می فرستند</string>
<string name="geohash_title">افشای مکان به عنوان </string>
<string name="geohash_explainer">یک از مکان شما به این بادداشت می افزاید. دیگران خواهند دانست که شما در فاصله 5 کیلومتری (3 مایلی) این مکان هستید.</string>
<string name="add_sensitive_content_explainer">پیش از نمایش محتوای شما، هشدار محتوای حساس به آن می افزاید. این برای هرگونه محتوای نامناسب برای محیط کار یا هر محتوای دیگری که برخی ممکن است توهین آمیز یا مشوش کننده بدانند خوب است.</string>
<string name="new_feature_nip24_might_not_be_available_title">ویژگی جدید</string>
<string name="new_feature_nip24_might_not_be_available_description">برای فعال سازی این حالت لازم است اماتیست یک پیغام NIP-24 بفرستد (پیغام های GiftWrapped, Sealed Direct and Group). مطمئن شوید که گیرنده از کلاینتی سازگاز استفاده می کند.</string>
<string name="new_feature_nip24_activate">فعال‌سازی</string>
<string name="messages_create_public_chat">عمومی</string>
<string name="messages_new_message">خصوصی</string>
<string name="messages_new_message_to">به</string>
<string name="messages_new_message_subject">موضوع</string>
<string name="messages_new_message_subject_caption">موضوع گفتگو</string>
<string name="messages_new_message_to_caption">"\@کاربر1, @کاربر2, @کاربر3"</string>
<string name="messages_group_descriptor">اعضای گروه</string>
<string name="messages_new_subject_message">توضیحات برای اعضا</string>
<string name="messages_new_subject_message_placeholder">تعویض نام به خاطر اهداف جدید.</string>
<string name="language_description">برای رابط کاربری اپ</string>
<string name="theme_description">پوسته مطابق سیستم، روشن، یا تاریک</string>
<string name="automatically_load_images_gifs_description">بارگیری خودکار تصاویر و جیف ها</string>
<string name="automatically_play_videos_description">پخش خودکار ویدئوها و جیف ها</string>
<string name="automatically_show_url_preview_description">نشان دادن پیش نمایش URL</string>
<string name="load_image_description">هنگام بارگیری تصاویر</string>
<string name="copy_url_to_clipboard">کپی URL به کلیپبورد</string>
<string name="copy_the_note_id_to_the_clipboard">کپی شناسه یادداشت به کلیپبورد</string>
<string name="created_at">ایجاد شده در</string>
<string name="rules">قوانین</string>
<string name="status_update">بارگذاری وضعیت</string>
<string name="lightning_wallets_not_found">خطا در تفسیر پیغام خطا</string>
<string name="poll_zap_value_min_max_explainer">آراء با مبلغ زپ سنجیده می شوند. می توانید برای جلوگیری از اسپم یک مبلغ حداقل تعیین کنید و برای جلوگیری از دستکاری نتیجه با زپ های بزرگ یک مبلغ حداکثر تعیین کنید. برای اینکه همه آراء ارزش یکسان داشته باشند در هر دو قسمت یک مبلغ را بزنید. یا این قسمت را خالی بگذارید تا هر مبلغ زپی را بپذیرد.</string>
<string name="error_dialog_zap_error">زپ فرستاده نمی شود</string>
<string name="error_dialog_talk_to_user">پیام به کاربر</string>
<string name="error_dialog_button_ok">قبول</string>
</resources>

View File

@@ -0,0 +1,487 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">Osoita QR-koodiin</string>
<string name="show_qr">Näytä QR</string>
<string name="profile_image">Profiilikuva</string>
<string name="scan_qr">Skannaa QR</string>
<string name="show_anyway">Näytä silti</string>
<string name="post_was_flagged_as_inappropriate_by">Viesti raportoitiin käyttäjän toimesta</string>
<string name="post_not_found">Tapahtumaa ladataan tai sitä ei löydy relelistalta</string>
<string name="channel_image">Kanavan kuva</string>
<string name="referenced_event_not_found">Viitattua tapahtumaa ei löytynyt</string>
<string name="could_not_decrypt_the_message">Viestin purku epäonnistui</string>
<string name="group_picture">Ryhmäkuva</string>
<string name="explicit_content">Julkeaa sisältöä</string>
<string name="spam">Roskaposti</string>
<string name="impersonation">Väärennös</string>
<string name="illegal_behavior">Laiton toiminta</string>
<string name="unknown">Tuntematon</string>
<string name="relay_icon">Relekuvake</string>
<string name="unknown_author">Tuntematon tekijä</string>
<string name="copy_text">Kopioi teksti</string>
<string name="copy_user_pubkey">Kopioi tekijän tunniste</string>
<string name="copy_note_id">Kopioi viestin tunniste</string>
<string name="broadcast">Lähetä</string>
<string name="request_deletion">Pyydä poistoa</string>
<string name="block_report">Estä / Ilmoita</string>
<string name="block_hide_user"><![CDATA[Estä & Piilota käyttäjä]]></string>
<string name="report_spam_scam">Ilmoita roskapostista / huijauksesta</string>
<string name="report_impersonation">Ilmoita väärennöksestä</string>
<string name="report_explicit_content">Ilmoita julkeasta sisällöstä</string>
<string name="report_illegal_behaviour">Ilmoita laittomasta toiminnasta</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Kirjaudu sisään yksityisellä avaimella viestehin vastaamiseksi</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Kirjaudu sisään yksityisellä avaimella tehostaaksesi viestejä</string>
<string name="login_with_a_private_key_to_like_posts">Kirjaudu sisään yksityisellä avaimella tykätäksesi viesteistä</string>
<string name="no_zap_amount_setup_long_press_to_change">Zap-määrää ei ole asetettu. Pidä painiketta pohjassa vaihtaaksesi sitä</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Kirjaudu sisään yksityisellä avaimella Zapien lähettämiseksi</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Kirjaudu sisään yksityisellä avaimella voidaksesi seurata</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Kirjaudu sisään yksityisellä avaimella seuraamisen lopettamiseksi</string>
<string name="zaps">Zapit</string>
<string name="view_count">Katselukerrat</string>
<string name="boost">Tehosta</string>
<string name="boosted">tehostettu</string>
<string name="quote">Lainaa</string>
<string name="new_amount_in_sats">Uusi määrä satseissa</string>
<string name="add">Lisää</string>
<string name="replying_to">"vastaus kohteeseen "</string>
<string name="and">" ja "</string>
<string name="in_channel">"kanavassa "</string>
<string name="profile_banner">Profiilin banneri</string>
<string name="payment_successful">Maksu onnistui</string>
<string name="error_parsing_error_message">Virhe virhesanomaa tulkittaessa</string>
<string name="following">" Seuratut"</string>
<string name="followers">" Seuraajat"</string>
<string name="profile">Profiili</string>
<string name="security_filters">Turvallisuussuodattimet</string>
<string name="log_out">Kirjaudu ulos</string>
<string name="show_more">Näytä lisää</string>
<string name="lightning_invoice">Lightning lasku</string>
<string name="pay">Maksa</string>
<string name="lightning_tips">Lightning tipit</string>
<string name="note_to_receiver">Viesti vastaanottajalle</string>
<string name="thank_you_so_much">Kiitos paljon!</string>
<string name="amount_in_sats">Määrä satseissa</string>
<string name="send_sats">Lähetä satseja</string>
<string name="error_parsing_preview_for">"Virhe tulkittaessa esikatselua kohteelle %1$s : %2$s"</string>
<string name="preview_card_image_for">"Esikatsele kortin kuvaa kohteelle %1$s"</string>
<string name="new_channel">Uusi kanava</string>
<string name="channel_name">Kanavan nimi</string>
<string name="my_awesome_group">Oma upea ryhmäni</string>
<string name="picture_url">Kuvan URL</string>
<string name="description">Kuvaus</string>
<string name="about_us">"Tietoa meistä.. "</string>
<string name="what_s_on_your_mind">Mitä mielessä?</string>
<string name="post">Lähetä</string>
<string name="save">Tallenna</string>
<string name="create">Luo</string>
<string name="cancel">Peruuta</string>
<string name="failed_to_upload_the_image">Kuvan lataus epäonnistui</string>
<string name="relay_address">Releosoite</string>
<string name="posts">Viestit</string>
<string name="bytes">Tavua</string>
<string name="errors">Virheet</string>
<string name="home_feed">Kotisyöte</string>
<string name="private_message_feed">Yksityinen viestisyöte</string>
<string name="public_chat_feed">Julkinen chat-syöte</string>
<string name="global_feed">Maailmanlaajuinen syöte</string>
<string name="search_feed">Hae syötettä</string>
<string name="add_a_relay">Lisää rele</string>
<string name="display_name">Näyttönimi</string>
<string name="my_display_name">Oma näyttönimeni</string>
<string name="username">Käyttäjänimi</string>
<string name="my_username">Oma käyttäjänimeni</string>
<string name="about_me">Tietoja minusta</string>
<string name="avatar_url">Avatarin URL</string>
<string name="banner_url">Bannerin URL</string>
<string name="website_url">Verkkosivuston URL</string>
<string name="ln_address">LN-osoite</string>
<string name="ln_url_outdated">LN-URL (vanhentunut)</string>
<string name="image_saved_to_the_gallery">Kuva tallennettu galleriaan</string>
<string name="failed_to_save_the_image">Kuvan tallennus epäonnistui</string>
<string name="upload_image">Lataa kuva</string>
<string name="uploading">Ladataan…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Käyttäjällä ei ole asetettuna Lightning-osoitetta satsien vastaanottamiseksi</string>
<string name="reply_here">"vastaa tähän.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Kopioi viestin tunniste leikepöydälle Nostressa jakamista varten</string>
<string name="copy_channel_id_note_to_the_clipboard">Kopioi kanavan tunniste (viesti) leikepöydälle</string>
<string name="edits_the_channel_metadata">Muokkaa kanavan metatietoja</string>
<string name="join">Liity</string>
<string name="known">Tunnetut</string>
<string name="new_requests">Uudet pyynnöt</string>
<string name="blocked_users">Estetyt käyttäjät</string>
<string name="new_threads">Uudet keskustelut</string>
<string name="conversations">Keskustelut</string>
<string name="notes">Viestit</string>
<string name="replies">Vastaukset</string>
<string name="follows">"Seuratut"</string>
<string name="reports">"Ilmiannetut"</string>
<string name="more_options">Lisää vaihtoehtoja</string>
<string name="relays">" Releet"</string>
<string name="website">Verkkosivusto</string>
<string name="lightning_address">Lightning-osoite</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Kopioi Nsec-tunniste (salasanasi) leikepöydälle varmuuskopiointia varten</string>
<string name="copy_private_key_to_the_clipboard">Kopioi yksityinen avain leikepöydälle</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Kopioi julkinen avain leikepöydälle jakamista varten</string>
<string name="copy_public_key_npub_to_the_clipboard">Kopioi julkinen avain (NPub) leikepöydälle</string>
<string name="send_a_direct_message">Lähetä suora viesti</string>
<string name="edits_the_user_s_metadata">Muokkaa käyttäjän metatietoja</string>
<string name="follow">Seuraa</string>
<string name="follow_back">Seuraa takaisin</string>
<string name="unblock">Poista esto</string>
<string name="copy_user_id">Kopioi käyttäjän tunniste</string>
<string name="unblock_user">Poista käyttäjän esto</string>
<string name="npub_hex_username">"npub, käyttäjänimi, teksti"</string>
<string name="clear">Tyhjennä</string>
<string name="app_logo">Sovelluksen logo</string>
<string name="nsec_npub_hex_private_key">nsec.. tai npub..</string>
<string name="show_password">Näytä salasana</string>
<string name="hide_password">Piilota salasana</string>
<string name="invalid_key">Virheellinen avain</string>
<string name="i_accept_the">"Hyväksyn "</string>
<string name="terms_of_use">käyttöehdot</string>
<string name="acceptance_of_terms_is_required">Käyttöehtojen hyväksyminen on pakollista</string>
<string name="key_is_required">Avain on pakollinen</string>
<string name="login">Kirjaudu sisään</string>
<string name="generate_a_new_key">Luo uusi avain</string>
<string name="loading_feed">Ladataan syöte</string>
<string name="error_loading_replies">"Virhe ladattaessa vastauksia: "</string>
<string name="try_again">Yritä uudelleen</string>
<string name="feed_is_empty">Syöte on tyhjä.</string>
<string name="refresh">Päivitä</string>
<string name="created">luotu</string>
<string name="with_description_of">kuvaus</string>
<string name="and_picture">ja kuva</string>
<string name="changed_chat_name_to">muutti keskustelun nimen kohteeksi</string>
<string name="description_to">kuvaus kohteeksi</string>
<string name="and_picture_to">ja kuva kohteeksi</string>
<string name="leave">Poistu</string>
<string name="unfollow">Lopeta seuraaminen</string>
<string name="channel_created">Kanava luotu</string>
<string name="channel_information_changed_to">"Kanavan tiedot muutettu kohteeksi"</string>
<string name="public_chat">Julkinen keskustelu</string>
<string name="posts_received">vastaanotetut viestit</string>
<string name="remove">Poista</string>
<string name="translations_auto">Automaattinen</string>
<string name="translations_translated_from">käännös kohteesta</string>
<string name="translations_to">kohteeseen</string>
<string name="translations_show_in_lang_first">Näytä ensin kielellä %1$s</string>
<string name="translations_always_translate_to_lang">Aina käännä kielelle %1$s</string>
<string name="translations_never_translate_from_lang">Älä koskaan käännä kielestä %1$s</string>
<string name="nip_05">Nostr-osoite</string>
<string name="never">ei koskaan</string>
<string name="now">nyt</string>
<string name="h">t</string>
<string name="m">m</string>
<string name="d">pv</string>
<string name="nudity">Alastomuus</string>
<string name="profanity_hateful_speech">Loukkaava kielenkäyttö / vihapuhe</string>
<string name="report_hateful_speech">Ilmoita vihapuheesta</string>
<string name="report_nudity_porn">Ilmoita alastomuudesta / pornosta</string>
<string name="others">muut</string>
<string name="mark_all_known_as_read">Merkitse kaikki Tunnetut luetuiksi</string>
<string name="mark_all_new_as_read">Merkitse kaikki Uudet luetuiksi</string>
<string name="mark_all_as_read">Merkitse kaikki luetuiksi</string>
<string name="backup_keys">Varmuuskopioi Avaimet</string>
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Avaimen varmuuskopiointi ja turvallisuusvinkit
\n\nTiliäsi suojaa salainen avain. Avain on pitkä satunnainen merkkijono, joka alkaa **nsec1**. Kaikki, joilla on pääsy salaiseen avaimeesi, voivat julkaista sisältöä käyttäen identiteettiäsi.
\n\n- Älä **koskaan** laita salaista avaintasi mihinkään verkkosivustoon tai ohjelmistoon, johon et luota.
\n- Amethyst-kehittäjät eivät **koskaan** kysy sinulta salaista avaintasi.
\n- **Säilytä** salaisen avaimen varmuuskopio turvallisesti tilin palauttamista varten. Suosittelemme salasanahallintaohjelman käyttämistä.
</string>
<string name="secret_key_copied_to_clipboard">Salainen avain (nsec) kopioitu leikepöydälle</string>
<string name="copy_my_secret_key">Kopioi salainen avain</string>
<string name="biometric_authentication_failed">Tunnistautuminen epäonnistui</string>
<string name="biometric_error">Virhe</string>
<string name="badge_created_by">"Luonut %1$s"</string>
<string name="badge_award_image_for">"Merkki kohteelle %1$s"</string>
<string name="new_badge_award_notif">Olet saanut uuden merkin</string>
<string name="award_granted_to">Merkki myönnetty kohteelle</string>
<string name="copied_note_text_to_clipboard">Viestin teksti kopioitu leikepöydälle</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Käyttäjän @npub kopioitu leikepöydälle</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Viestin tunniste (@note1) kopioitu leikepöydälle</string>
<string name="select_text_dialog_top">Valitse teksti</string>
<string name="private_conversation_notification">"&lt;Ei voitu purkaa yksityisviestiä&gt;\n\nSinut mainittiin yksityisessä/salatussa keskustelussa kohteiden %1$s ja %2$s välillä."</string>
<string name="account_switch_add_account_dialog_title">Lisää uusi tili</string>
<string name="drawer_accounts">Tilit</string>
<string name="account_switch_select_account">Valitse tili</string>
<string name="account_switch_add_account_btn">Lisää uusi tili</string>
<string name="account_switch_active_account">Aktiivinen tili</string>
<string name="account_switch_has_private_key">Sisältää salaisen avaimen</string>
<string name="account_switch_pubkey_only">Vain julkisavain, ei salaista avainta</string>
<string name="back">Takaisin</string>
<string name="quick_action_select">Valitse</string>
<string name="quick_action_share_browser_link">Jaa selaimen linkki</string>
<string name="quick_action_share">Jaa</string>
<string name="quick_action_copy_user_id">Kirjoittajan tunniste</string>
<string name="quick_action_copy_note_id">Viestin tunniste</string>
<string name="quick_action_copy_text">Kopioi teksti</string>
<string name="quick_action_delete">Poista</string>
<string name="quick_action_unfollow">Lopeta seuraaminen</string>
<string name="quick_action_follow">Seuraa</string>
<string name="quick_action_request_deletion_alert_title">Pyydä poistoa</string>
<string name="quick_action_request_deletion_alert_body">Amethyst pyytää, että viestisi poistetaan releistä, joihin olet tällä hetkellä yhteydessä. Emme voi taata, että viestisi poistetaan pysyvästi niistä releistä tai muista releistä, joissa se voi olla tallennettuna.</string>
<string name="quick_action_block_dialog_btn">Estä</string>
<string name="quick_action_delete_dialog_btn">Poista</string>
<string name="quick_action_block">Estä</string>
<string name="quick_action_report">Ilmoita</string>
<string name="quick_action_delete_button">Poista</string>
<string name="quick_action_dont_show_again_button">Älä näytä uudelleen</string>
<string name="report_dialog_spam">Roskaposti tai huijaus</string>
<string name="report_dialog_profanity">Loukkaava kielenkäyttö tai vihamielinen käytös</string>
<string name="report_dialog_impersonation">Haitallinen henkilöllisyyden omiminen</string>
<string name="report_dialog_nudity">Alastomuus tai graafinen sisältö</string>
<string name="report_dialog_illegal">Laiton käytös</string>
<string name="report_dialog_blocking_a_user">Käyttäjän estäminen piilottaa hänen sisältönsä sovelluksessasi. Merkintäsi ovat silti julkisesti nähtävillä, myös niille, jotka estät. Estetyt käyttäjät on lueteltu turvallisuussuodattimien näytössä.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Estä ja piilota käyttäjä]]></string>
<string name="report_dialog_report_btn">Ilmoita väärinkäytöstä</string>
<string name="report_dialog_reminder_public">Kaikki ilmoitukset ovat julkisesti nähtävillä.</string>
<string name="report_dialog_additional_reason_placeholder">Voit antaa lisäkontekstia ilmoituksellesi valinnaisesti...</string>
<string name="report_dialog_additional_reason_label">Lisäkonteksti</string>
<string name="report_dialog_select_reason_label">Syy</string>
<string name="report_dialog_select_reason_placeholder">Valitse syy...</string>
<string name="report_dialog_post_report_btn">Lähetä ilmoitus</string>
<string name="report_dialog_title">Estä ja ilmoita</string>
<string name="block_only">Estä</string>
<string name="bookmarks">Kirjanmerkit</string>
<string name="private_bookmarks">Yksityiset kirjanmerkit</string>
<string name="public_bookmarks">Julkiset kirjanmerkit</string>
<string name="add_to_private_bookmarks">Lisää yksityisiin kirjanmerkkeihin</string>
<string name="add_to_public_bookmarks">Lisää julkisiin kirjanmerkkeihin</string>
<string name="remove_from_private_bookmarks">Poista yksityisistä kirjanmerkeistä</string>
<string name="remove_from_public_bookmarks">Poista julkisista kirjanmerkeistä</string>
<string name="wallet_connect_service">Lompakkoyhteyspalvelu</string>
<string name="wallet_connect_service_explainer">Valtuuttaa Nostr-salaisuuden maksamaan zapeja ilman sovelluksesta poistumista. Pidä salaisuutesi turvassa ja käytä yksityistä relettä, jos mahdollista.</string>
<string name="wallet_connect_service_pubkey">Lompakkoyhteyden julkinen avain</string>
<string name="wallet_connect_service_relay">Lompakkoyhteyden rele</string>
<string name="wallet_connect_service_secret">Lompakkoyhteyden salainen avain</string>
<string name="wallet_connect_service_show_secret">Näytä salainen avain</string>
<string name="wallet_connect_service_secret_placeholder">nsec / heksadesimaalinen salainen avain</string>
<string name="pledge_amount_in_sats">Luvattu määrä satseissa</string>
<string name="post_poll">Luo äänestys</string>
<string name="poll_heading_required">Vaaditut kohdat:</string>
<string name="poll_zap_recipients">Zap-vastaanottajat</string>
<string name="poll_primary_description">Pääasiallinen äänestyksen kuvaus...</string>
<string name="poll_option_index">Vaihtoehto %s</string>
<string name="poll_option_description">Vaihtoehdon kuvaus</string>
<string name="poll_heading_optional">Valinnaiset kohdat:</string>
<string name="poll_zap_value_min">Vähimmäiszap</string>
<string name="poll_zap_value_max">Enimmäiszap</string>
<string name="poll_consensus_threshold">Konsensus</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Sulje äänestys</string>
<string name="poll_closing_time_days">päivän kuluttua</string>
<string name="poll_is_closed">Äänestys on suljettu uusilta ääniltä</string>
<string name="poll_zap_amount">Zap-määrä</string>
<string name="one_vote_per_user_on_atomic_votes">Vain yksi ääni käyttäjää kohti tällä äänestystyypillä</string>
<string name="looking_for_event">"Etsitään tapahtumaa %1$s"</string>
<string name="custom_zaps_add_a_message">Lisää julkinen viesti</string>
<string name="custom_zaps_add_a_message_private">Lisää yksityinen viesti</string>
<string name="custom_zaps_add_a_message_nonzap">Lisää laskuviesti</string>
<string name="custom_zaps_add_a_message_example">Kiitos tehdystä työstä!</string>
<string name="lightning_create_and_add_invoice">Luo ja lisää</string>
<string name="poll_author_no_vote">Äänestyksen luojat eivät voi äänestää omissa äänestyksissään.</string>
<string name="hash_verification_passed">Tämä sisältö on sama kuin viestin julkaisuhetkellä</string>
<string name="hash_verification_failed">Tämä sisältö on muuttunut. Kirjoittaja ei ehkä ole nähnyt tai hyväksynyt muutosta</string>
<string name="content_description_add_image">Lisää kuva</string>
<string name="content_description_add_video">Lisää video</string>
<string name="content_description_add_document">Lisää dokumentti</string>
<string name="add_content">Lisää viestiin</string>
<string name="content_description">Sisällön kuvaus</string>
<string name="content_description_example">Sininen vene valkoisella hiekkarannalla auringonlaskussa</string>
<string name="zap_type">Zap-tyyppi</string>
<string name="zap_type_explainer">Zap-tyyppi kaikille vaihtoehdoille</string>
<string name="zap_type_public">Julkinen</string>
<string name="zap_type_public_explainer">Kaikki voivat nähdä tapahtuman ja viestin</string>
<string name="zap_type_private">Yksityinen</string>
<string name="zap_type_private_explainer">Lähettäjä ja vastaanottaja voivat nähdä toisensa ja lukea viestin</string>
<string name="zap_type_anonymous">Anonyymi</string>
<string name="zap_type_anonymous_explainer">Vastaanottaja ja yleisö eivät tiedä, kuka lähetti maksun</string>
<string name="zap_type_nonzap">Ei-Zap</string>
<string name="zap_type_nonzap_explainer">Ei jälkiä Nostrissa, vain Lightningissa</string>
<string name="file_server">Tiedostopalvelin</string>
<string name="zap_forward_lnAddress">LnAddress tai @Käyttäjä</string>
<string name="upload_server_imgur">imgur.com - luotettava</string>
<string name="upload_server_imgur_explainer">Imgur voi muokata tiedostoa</string>
<string name="upload_server_nostrimg">nostrimg.com - luotettava</string>
<string name="upload_server_nostrimg_explainer">NostrImg voi muokata tiedostoa</string>
<string name="upload_server_nostrbuild">nostr.build - luotettava</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build voi muokata tiedostoa</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - luotettava</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev voi muokata tiedostoa</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - luotettava</string>
<string name="upload_server_nostrcheckme_explainer">Nostrcheck.me voi muokata tiedostoa</string>
<string name="upload_server_imgur_nip94">Todennettava Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Tarkistaa, onko Imgur muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
<string name="upload_server_nostrimg_nip94">Todennettava NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Tarkistaa, onko NostrImg muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
<string name="upload_server_nostrbuild_nip94">Todennettava Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Tarkistaa, onko Nostr.build muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
<string name="upload_server_nostrfilesdev_nip94">Todennettava Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Tarkistaa, onko Nostrfiles.dev muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
<string name="upload_server_nostrcheckme_nip94">Todennettava Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Tarkistaa, onko Nostrcheck.me muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
<string name="upload_server_relays_nip95">Omat releet (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Tiedostot ovat isännöityinä omilla releilläsi. Uusi NIP: tarkista, tukevatko ne sitä</string>
<string name="connect_via_tor_short">Tor/Orbot-asetus</string>
<string name="connect_via_tor">Yhdistä Orbot-asetuksesi kautta</string>
<string name="do_you_really_want_to_disable_tor_title">Haluatko todella katkaista Orbot/Tor-yhteytesi?</string>
<string name="do_you_really_want_to_disable_tor_text">Tietosi siirtyvät välittömästi tavalliseen verkkoon</string>
<string name="yes">Kyllä</string>
<string name="no">Ei</string>
<string name="follow_list_selection">Seuraa-lista</string>
<string name="follow_list_kind3follows">Kaikki seuraajat</string>
<string name="follow_list_global">Maailmanlaajuinen</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Yhdistä Torin kautta Orbotin avulla
\n\n1. Asenna [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Käynnistä Orbot
\n3. Orbotissa tarkista Socks-portti. Oletusarvo on 9050
\n4. Tarvittaessa vaihda porttia Orbotissa
\n5. Aseta Socks-portti tällä näytöllä
\n6. Paina Aktivoi-painiketta käyttääksesi Orbotia välityspalvelimena
</string>
<string name="orbot_socks_port">Orbot Socks-portti</string>
<string name="invalid_port_number">Virheellinen porttinumero</string>
<string name="use_orbot">Käytä Orbotia</string>
<string name="disconnect_from_your_orbot_setup">Katkaise Tor/Orbot-yhteys</string>
<string name="app_notification_dms_channel_name">Yksityisviestit</string>
<string name="app_notification_dms_channel_description">Ilmoittaa sinulle, kun yksityinen viesti saapuu</string>
<string name="app_notification_zaps_channel_name">Saapuneet Zapit</string>
<string name="app_notification_zaps_channel_description">Ilmoittaa sinulle, kun joku zappaa sinua</string>
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">Lähettäjä: %1$s</string>
<string name="app_notification_zaps_channel_message_for">saaja: %1$s</string>
<string name="reply_notify">Ilmoitus: </string>
<string name="channel_list_join_conversation">Liity keskusteluun</string>
<string name="channel_list_user_or_group_id">Käyttäjän tai ryhmän tunniste</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent tai heksadesimaali</string>
<string name="channel_list_create_channel">Luo</string>
<string name="channel_list_join_channel">Liity</string>
<string name="today">Tänään</string>
<string name="content_warning">Sisältövaroitus</string>
<string name="content_warning_explanation">Tämä viesti sisältää herkkää sisältöä, joka voi olla loukkaavaa tai häiritsevää joillekin ihmisille</string>
<string name="content_warning_hide_all_sensitive_content">Piilota aina herkkä sisältö</string>
<string name="content_warning_show_all_sensitive_content">Näytä aina herkkä sisältö</string>
<string name="content_warning_see_warnings">Näytä aina sisältövaroitukset</string>
<string name="recommended_apps">Suosittelee: </string>
<string name="filter_spam_from_strangers">Suodata roskapostia tuntemattomilta</string>
<string name="warn_when_posts_have_reports_from_your_follows">Varoita, kun seurattavasi ovat raportoineet viestejä</string>
<string name="new_reaction_symbol">Uusi reaktiosymboli</string>
<string name="no_reaction_type_setup_long_press_to_change">Ei valittu reaktiotyyppejä. Pidä pitkään painettuna vaihtaaksesi.</string>
<string name="zapraiser">Zapkerääjä</string>
<string name="zapraiser_explainer">Lisää tavoitesumma satseina tämän viestin tukemiseksi. Tuettavat asiakkaat voivat näyttää tämän edistymispalkkina tuen kannustamiseksi</string>
<string name="zapraiser_target_amount_in_sats">Tavoitesumma satseina</string>
<string name="sats_to_complete">Zapraiser %1$s. %2$s satsiin tavoitteesta</string>
<string name="read_from_relay">Lue releestä</string>
<string name="write_to_relay">Kirjoita releeseen</string>
<string name="an_error_occurred_trying_to_get_relay_information">Virhe yritettäessä hakea rele-tietoja %1$s:stä</string>
<string name="owner">Omistaja</string>
<string name="version">Versio</string>
<string name="software">Ohjelmisto</string>
<string name="contact">Yhteystiedot</string>
<string name="supports">Tuetut NIPit</string>
<string name="admission_fees">Pääsymaksut</string>
<string name="payments_url">Maksut URL</string>
<string name="limitations">Rajoitukset</string>
<string name="countries">Maat</string>
<string name="languages">Kielet</string>
<string name="tags">Tagit</string>
<string name="posting_policy">Julkaisupolitiikka</string>
<string name="message_length">Viestin pituus</string>
<string name="subscriptions">Tilaukset</string>
<string name="filters">Suodattimet</string>
<string name="subscription_id_length">Tilaustunnuksen pituus</string>
<string name="minimum_prefix">Vähimmäisliite</string>
<string name="maximum_event_tags">Maksimi tapahtumien tagit</string>
<string name="content_length">Sisällön pituus</string>
<string name="minimum_pow">Vähimmäis PoW</string>
<string name="auth">Autentikointi</string>
<string name="payment">Maksu</string>
<string name="cashu">Cashu-tunniste</string>
<string name="cashu_redeem">Lunasta</string>
<string name="no_lightning_address_set">Ei Lightning-osoitetta asetettuna</string>
<string name="copied_token_to_clipboard">Kopioitu tunniste leikepöydälle</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">POISSA</string>
<string name="live_stream_ended_tag">PÄÄTTYNYT</string>
<string name="live_stream_planned_tag">AJOITETTU</string>
<string name="live_stream_is_offline">Livestream on offline-tilassa</string>
<string name="live_stream_has_ended">Livestream on päättynyt</string>
<string name="are_you_sure_you_want_to_log_out">Uloskirjautuminen poistaa kaikki paikalliset tiedot. Varmista, että sinulla on varmuuskopio yksityisavaimistasi välttääksesi tilisi menetyksen. Haluatko jatkaa?</string>
<string name="followed_tags">Seuratut tagit</string>
<string name="relay_setup">Releet</string>
<string name="discover_live">Live</string>
<string name="discover_community">Yhteisö</string>
<string name="discover_chat">Keskustelut</string>
<string name="community_approved_posts">Hyväksytyt viestit</string>
<string name="groups_no_descriptor">Tällä ryhmällä ei ole kuvausta tai sääntöjä. Keskustele omistajan kanssa sen lisäämiseksi</string>
<string name="community_no_descriptor">Tällä yhteisöllä ei ole kuvausta. Keskustele omistajan kanssa sen lisäämiseksi</string>
<string name="add_sensitive_content_label">Herkkä sisältö</string>
<string name="add_sensitive_content_description">Lisää herkän sisällön varoitus ennen tämän sisällön näyttämistä</string>
<string name="settings">Asetukset</string>
<string name="connectivity_type_always">Aina</string>
<string name="connectivity_type_wifi_only">Vain WiFi</string>
<string name="connectivity_type_never">Ei koskaan</string>
<string name="system">Järjestelmä</string>
<string name="light">Vaalea</string>
<string name="dark">Tumma</string>
<string name="application_preferences">Sovelluksen asetukset</string>
<string name="language">Kieli</string>
<string name="theme">Teema</string>
<string name="automatically_load_images_gifs">Kuvan esikatselu</string>
<string name="automatically_play_videos">Videon toisto</string>
<string name="automatically_show_url_preview">URL-esikatselu</string>
<string name="load_image">Lataa kuva</string>
<string name="spamming_users">Roskapostittajat</string>
<string name="muted_button">Ääni pois päältä. Napsauta palauttaaksesi äänet</string>
<string name="mute_button">Ääni päällä. Napsauta vaimentaaksesi</string>
<string name="search_button">Hae paikallisia ja etämerkintöjä</string>
<string name="nip05_verified">Nostr-osoite varmennettiin</string>
<string name="nip05_failed">Nostr-osoite ei läpäissyt varmennusta</string>
<string name="nip05_checking">Tarkistetaan Nostr-osoite</string>
<string name="select_deselect_all">Valitse/Poista valinta kaikista</string>
<string name="default_relays">Oletus</string>
<string name="select_a_relay_to_continue">Valitse rele jatkaaksesi</string>
<string name="zap_forward_title">Ohjaa zappit kohteeseen:</string>
<string name="zap_forward_explainer">Sovelluksien tukeminen ohjaa zappit alla olevaan LN-osoitteeseen tai käyttäjäprofiiliin sen sijaan, että ne menisivät sinulle</string>
<string name="geohash_title">Näytä sijaintina </string>
<string name="geohash_explainer">Lisää sijaintisi Geohashinä viestiin. On julkisesti nähtävänä, että olet 5 km (3 mailin) säteellä nykyisestä sijainnista</string>
<string name="add_sensitive_content_explainer">Lisää herkkä sisältö -varoitus ennen sisällön näyttämistä. Tämä on suositeltua kaikelle aikuissisällölle tai sisällölle, jota jotkut saattavat pitää loukkaavana tai häiritsevänä</string>
<string name="new_feature_nip24_might_not_be_available_title">Uusi ominaisuus</string>
<string name="new_feature_nip24_might_not_be_available_description">Tämän tilan aktivoiminen vaatii Amethystin lähettämään NIP-24-viestin (Lahjapakatut, Suljetut suorat ja ryhmäviestit). NIP-24 on uusi, ja useimmat asiakkaat eivät ole vielä toteuttaneet sitä. Varmista, että vastaanottaja käyttää yhteensopivaa asiakasta.</string>
<string name="new_feature_nip24_activate">Aktivoi</string>
<string name="messages_create_public_chat">Julkinen</string>
<string name="messages_new_message">Yksityinen</string>
<string name="messages_new_message_to">Vastaanottaja</string>
<string name="messages_new_message_subject">Aihe</string>
<string name="messages_new_message_subject_caption">Keskustelun aihe</string>
<string name="messages_new_message_to_caption">"\@Käyttäjä1, @Käyttäjä2, @Käyttäjä3"</string>
<string name="messages_group_descriptor">Tämän ryhmän jäsenet</string>
<string name="messages_new_subject_message">Selitys jäsenille</string>
<string name="messages_new_subject_message_placeholder">Muuta nimi uusille tavoitteille.</string>
<string name="language_description">Sovelluksen käyttöliittymälle</string>
<string name="theme_description">Tumma, vaalea tai järjestelmäteema</string>
<string name="automatically_load_images_gifs_description">Lataa kuvat ja GIF-animaatiot automaattisesti</string>
<string name="automatically_play_videos_description">Toista videot ja GIF-animaatiot automaattisesti</string>
<string name="automatically_show_url_preview_description">Näytä URL-esikatselut</string>
<string name="load_image_description">Koska kuvat ladataan</string>
<string name="copy_url_to_clipboard">Kopioi URL leikepöydälle</string>
<string name="copy_the_note_id_to_the_clipboard">Kopioi viestin tunniste leikepöydälle</string>
<string name="created_at">Luotu</string>
<string name="rules">Säännöt</string>
<string name="status_update">Päivitä tilaasi</string>
<string name="lightning_wallets_not_found">Virhe tulkittaessa virhesanomaa</string>
<string name="poll_zap_value_min_max_explainer">Äänet painotetaan zappin määrän perusteella. Voit asettaa minimimäärän roskapostin välttämiseksi ja maksimimäärän suurten zappereiden estämiseksi. Käytä samaa määrää molemmissa kentissä, jotta jokainen ääni arvostetaan samalla määrällä. Jätä se tyhjäksi hyväksyäksesi minkä tahansa määrän.</string>
<string name="error_dialog_zap_error">Zapin lähettäminen ei onnistunut</string>
<string name="error_dialog_talk_to_user">Viesti käyttäjälle</string>
<string name="error_dialog_button_ok">Ok</string>
<string name="relay_information_document_error_assemble_url">Kohdetta %1$s ei tavoitettu: %2$s</string>
<string name="relay_information_document_error_reach_server">Kohdetta %1$s ei tavoitettu: %2$s</string>
<string name="relay_information_document_error_parse_result">Tuloksen jäsentäminen epäonnistui %1$s: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s epäonnistui koodilla %2$s</string>
<string name="active_for">Aktiivinen kohteelle: </string>
<string name="active_for_home">Koti</string>
<string name="active_for_msg">YVt</string>
<string name="active_for_chats">Keskustelut</string>
<string name="active_for_global">Yleinen</string>
<string name="active_for_search">Haku</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

View File

@@ -5,11 +5,11 @@
<string name="profile_image">Image de profil</string>
<string name="scan_qr">Scanner le QR code</string>
<string name="show_anyway">Montrer quand même</string>
<string name="post_was_flagged_as_inappropriate_by">Post signalé innaproprié par</string>
<string name="post_was_flagged_as_inappropriate_by">Post signalé inapproprié par</string>
<string name="post_not_found">Post non trouvé</string>
<string name="channel_image">Image du canal</string>
<string name="referenced_event_not_found">référence de l\'évènement non trouvée</string>
<string name="could_not_decrypt_the_message">Impossibe de déchiffrer le message</string>
<string name="could_not_decrypt_the_message">Impossible de déchiffrer le message</string>
<string name="group_picture">Image de groupe</string>
<string name="explicit_content">Contenu choquant</string>
<string name="spam">Spam</string>
@@ -22,8 +22,10 @@
<string name="copy_user_pubkey">Copier la clé publique de l\'utilisateur</string>
<string name="copy_note_id">Copier l\'ID de la note</string>
<string name="broadcast">Diffuser</string>
<string name="block_hide_user">Bloquer et cacher l\'utilisateur</string>
<string name="report_spam_scam">Signaler un Spam / Scam</string>
<string name="request_deletion">Demande de suppression</string>
<string name="block_report">Bloquer / Signaler</string>
<string name="block_hide_user"><![CDATA[Bloquer et cacher l\'utilisateur]]></string>
<string name="report_spam_scam">Signaler Spam / Arnaque</string>
<string name="report_impersonation">Signaler une falsification d\'identité</string>
<string name="report_explicit_content">Signaler un contenu choquant</string>
<string name="report_illegal_behaviour">Signaler un comportement illégal</string>
@@ -32,6 +34,8 @@
<string name="login_with_a_private_key_to_like_posts">Connectez vous avec une clé privée pour pouvoir aimer des posts</string>
<string name="no_zap_amount_setup_long_press_to_change">Aucun montant sélectionné pour le zap. Appui long pour modifier</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Connectez vous avec une clé privée pour pouvoir envoyer des zaps</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Connectez-vous avec une clé privée pour Suivre</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Connectez-vous avec une clé privée pour ne plus Suivre</string>
<string name="zaps">Zaps</string>
<string name="view_count">Nombre de vues</string>
<string name="boost">Boost</string>
@@ -43,9 +47,11 @@
<string name="and">" et "</string>
<string name="in_channel">"dans le canal "</string>
<string name="profile_banner">Bannière du profil</string>
<string name="payment_successful">Paiement Réussi</string>
<string name="error_parsing_error_message">Erreur d\'analyse du message d\'erreur</string>
<string name="following">" Abonnements"</string>
<string name="followers">" Abonnés"</string>
<string name="profile">Profile</string>
<string name="profile">Profil</string>
<string name="security_filters">Filtres de sécurité</string>
<string name="log_out">Déconnexion</string>
<string name="show_more">Voir plus</string>
@@ -53,17 +59,17 @@
<string name="pay">Payer</string>
<string name="lightning_tips">Pourboire Lightning</string>
<string name="note_to_receiver">Note au destinataire</string>
<string name="thank_you_so_much">Merci beaucoup!</string>
<string name="thank_you_so_much">Merci beaucoup !</string>
<string name="amount_in_sats">Montant en Sats</string>
<string name="send_sats">Envoyer des Sats</string>
<string name="error_parsing_preview_for">"Erreur d\'analyse de l\'aperçu pour %1$s : %2$s"</string>
<string name="preview_card_image_for">"Aperçu de l\'image pour %1$s"</string>
<string name="error_parsing_preview_for">"Erreur d'analyse de l'aperçu pour %1$s : %2$s"</string>
<string name="preview_card_image_for">"Aperçu de l'image pour %1$s"</string>
<string name="new_channel">Nouveau canal</string>
<string name="channel_name">Nom de canal</string>
<string name="my_awesome_group">Mon groupe spectaculaire</string>
<string name="picture_url">Url de l\'image</string>
<string name="description">Description</string>
<string name="about_us">à propos</string>
<string name="about_us">"à propos"</string>
<string name="what_s_on_your_mind">Quoi de neuf ?</string>
<string name="post">Envoyer</string>
<string name="save">Sauvegarder</string>
@@ -72,6 +78,7 @@
<string name="failed_to_upload_the_image">Échec du téléchargement de l\'image</string>
<string name="relay_address">Adresse du relais</string>
<string name="posts">Publications</string>
<string name="bytes">Octets</string>
<string name="errors">Erreurs</string>
<string name="home_feed">Vos notifications</string>
<string name="private_message_feed">Flux de messages privés</string>
@@ -94,7 +101,7 @@
<string name="upload_image">Charger une image</string>
<string name="uploading">Chargement…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
<string name="reply_here">"Répondre ici.. "</string>
<string name="reply_here">"répondre ici"</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr</string>
<string name="copy_channel_id_note_to_the_clipboard">Copier l\'ID de la chaîne (Note) dans le presse-papiers</string>
<string name="edits_the_channel_metadata">Modifie les métadonnées du canal</string>
@@ -119,17 +126,18 @@
<string name="send_a_direct_message">Envoyer un message direct</string>
<string name="edits_the_user_s_metadata">Modifie les métadonnées de l\'utilisateur</string>
<string name="follow">Suivre</string>
<string name="follow_back">Suivre en retour</string>
<string name="unblock">Débloquer</string>
<string name="copy_user_id">Copier l\'ID utilisateur</string>
<string name="unblock_user">Débloquer l\'utilisateur</string>
<string name="npub_hex_username">"npub, hex, nom d\'utilisateur "</string>
<string name="npub_hex_username">"npub, hex, nom d'utilisateur "</string>
<string name="clear">Nettoyer</string>
<string name="app_logo">Logo de l\'App</string>
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string>
<string name="show_password">Révéler le mot de passe</string>
<string name="hide_password">Cacher le mot de passe</string>
<string name="invalid_key">Clé non valide</string>
<string name="i_accept_the">"J\'accepte les "</string>
<string name="i_accept_the">"J'accepte les "</string>
<string name="terms_of_use">conditions d\'utilisation</string>
<string name="acceptance_of_terms_is_required">L\'acceptation des conditions est requise</string>
<string name="key_is_required">La clé est requise</string>
@@ -137,7 +145,7 @@
<string name="generate_a_new_key">Générer une nouvelle clé</string>
<string name="loading_feed">Chargement du flux</string>
<string name="error_loading_replies">"Erreur lors du chargement des réponses : "</string>
<string name="try_again">essayer à nouveau</string>
<string name="try_again">Essayer à nouveau</string>
<string name="feed_is_empty">Le flux est vide.</string>
<string name="refresh">Rafraîchir</string>
<string name="created">créé</string>
@@ -159,6 +167,7 @@
<string name="translations_show_in_lang_first">Montre en %1$s en premier</string>
<string name="translations_always_translate_to_lang">Toujours traduire en %1$s</string>
<string name="translations_never_translate_from_lang">Ne jamais traduire depuis %1$s</string>
<string name="nip_05">Adresse Nostr</string>
<string name="never">jamais</string>
<string name="now">maintenant</string>
<string name="h">h</string>
@@ -173,8 +182,7 @@
<string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string>
<string name="mark_all_as_read">Tout marquer comme lu</string>
<string name="backup_keys">Clés de sauvegarde</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Sauvegarde des clés et conseils de sécurité
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Sauvegarde des clés et conseils de sécurité
\n\nVotre compte est sécurisé par une clé secrète. La clé est une longue chaîne aléatoire commençant par **nsec1**. Toute personne ayant accès à votre clé secrète peut publier du contenu en utilisant votre identité.
\n\n- Ne communiquez **jamais** votre clé secrète à un site Web ou un logiciel auquel vous ne faites pas confiance.
\n- Les développeurs d\'Amethyst ne vous demanderont **jamais** votre clé secrète.
@@ -192,7 +200,6 @@
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copie du @npub utilisateur dans le presse-papiers</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Copie de l\'ID de la note (@note1) dans le presse-papiers</string>
<string name="select_text_dialog_top">Sélectionner le Texte</string>
<string name="private_conversation_notification">"&lt;Impossible de déchiffrer le message privé&gt;\n\nVous avez été cité dans une conversation privée/cryptée entre %1$s et %2$s."</string>
<string name="account_switch_add_account_dialog_title">Ajouter un Nouveau Compte</string>
<string name="drawer_accounts">Comptes</string>
@@ -235,8 +242,6 @@
<string name="report_dialog_post_report_btn">Publier le signalement</string>
<string name="report_dialog_title">Bloquer et Signaler</string>
<string name="block_only">Bloquer</string>
<string name="bookmarks">Favoris</string>
<string name="private_bookmarks">Favoris Privés</string>
<string name="public_bookmarks">Favoris Publics</string>
@@ -244,15 +249,13 @@
<string name="add_to_public_bookmarks">Ajouter aux Favoris Publics</string>
<string name="remove_from_private_bookmarks">Supprimer des Favoris Privés</string>
<string name="remove_from_public_bookmarks">Supprimer des Favoris Publics</string>
<string name="wallet_connect_service">Service Wallet Connect</string>
<string name="wallet_connect_service_explainer">Autorise un Nostr Secret à payer des zaps sans quitter l\'application. Gardez le secret en toute sécurité et utilisez un relais privé si possible</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
<string name="wallet_connect_service_pubkey">Clé publique Wallet Connect</string>
<string name="wallet_connect_service_relay">Relais Wallet Connect</string>
<string name="wallet_connect_service_secret">Secret Wallet Connect</string>
<string name="wallet_connect_service_show_secret">Montrer la clé secrète</string>
<string name="wallet_connect_service_secret_placeholder">Clé privée nsec / hex</string>
<string name="pledge_amount_in_sats">Montant engagé en Sats</string>
<string name="post_poll">Message Sondage</string>
<string name="poll_heading_required">Champs requis:</string>
@@ -260,7 +263,7 @@
<string name="poll_primary_description">Description du sondage primaire…</string>
<string name="poll_option_index">Option %s</string>
<string name="poll_option_description">Description de l\'option du Sondage</string>
<string name="poll_heading_optional">Champs optionels:</string>
<string name="poll_heading_optional">Champs optionnels :</string>
<string name="poll_zap_value_min">Zap minimum</string>
<string name="poll_zap_value_max">Zap maximum</string>
<string name="poll_consensus_threshold">Consensus</string>
@@ -270,97 +273,65 @@
<string name="poll_is_closed">Le Sondage est fermé aux nouveaux votes</string>
<string name="poll_zap_amount">Montant des Zap</string>
<string name="one_vote_per_user_on_atomic_votes">Un seul vote par utilisateur est autorisé sur ce type de sondage</string>
<string name="looking_for_event">"Recherche de l'Evénement %1$s"</string>
<string name="looking_for_event">"Recherche de l'Événement %1$s"</string>
<string name="custom_zaps_add_a_message">Ajouter un message public</string>
<string name="custom_zaps_add_a_message_private">Ajouter un message privé</string>
<string name="custom_zaps_add_a_message_nonzap">Ajouter un message facture</string>
<string name="custom_zaps_add_a_message_example">Merci pour tout votre travail !</string>
<string name="lightning_create_and_add_invoice">Créer et Ajouter</string>
<string name="poll_author_no_vote">Les auteurs du sondage ne peuvent pas voter dans leurs propres sondages.</string>
<string name="hash_verification_passed">Ce contenu est le même depuis le message</string>
<string name="hash_verification_failed">Ce contenu a changé. L\'auteur n\'a peut-être pas vu ou approuvé le changement</string>
<string name="content_description_add_image">Ajouter une Image</string>
<string name="content_description_add_video">Ajouter une Vidéo</string>
<string name="content_description_add_document">Ajouter un Document</string>
<string name="add_content">Ajouter au Message</string>
<string name="content_description">Description des contenus</string>
<string name="content_description_example">Un bateau bleu sur une plage de sable blanc au coucher du soleil</string>
<string name="zap_type">Type de Zap</string>
<string name="zap_type_explainer">Zap Type pour toutes les options</string>
<string name="zap_type_public">Public</string>
<string name="zap_type_public_explainer">Tout le monde peut voir la transaction et le message</string>
<string name="zap_type_private">Privé</string>
<string name="zap_type_private_explainer">L\'expéditeur et le receveur peuvent se voir et lire le message</string>
<string name="zap_type_anonymous">Anonyme</string>
<string name="zap_type_anonymous_explainer">Le receveur et le public ne savent pas qui a envoyé le paiement</string>
<string name="zap_type_nonzap">Non-Zap</string>
<string name="zap_type_nonzap_explainer">Aucune trace sur Nostr, seulement sur le Lightning</string>
<string name="file_server">Serveur fichier</string>
<string name="zap_forward_lnAddress">Adresse LN ou @Utilisateur</string>
<string name="upload_server_imgur">imgur.com - de confiance</string>
<string name="upload_server_imgur_explainer">Imgur peut modifier le fichier</string>
<string name="upload_server_nostrimg">nostrimg.com - de confiance</string>
<string name="upload_server_nostrimg_explainer">NostrImg peut modifier le fichier</string>
<string name="upload_server_nostrbuild">nostr.build - de confiance</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build peut modifier le fichier</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - de confiance</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev peut modifier le fichier</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - de confiance</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me peut modifier le fichier</string>
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94">Imgur vérifiable (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Vérifie si Imgur a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94">NostrImg vérifiable (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Vérifie si NostrImg a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94">Nostr.build vérifiable (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Vérifie si Nostr.build a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
<string name="upload_server_nostrfilesdev_nip94">Verifiable Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev vérifiable (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Vérifie si Nostrfiles.dev a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me vérifiable (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Vérifie si Nostrcheck.me a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
<string name="connect_via_tor">Se connecter à travers la configuration Orbot</string>
<string name="do_you_really_want_to_disable_tor_title">Se déconnecter de Orbot/Tor?</string>
<string name="do_you_really_want_to_disable_tor_text">Vos données seront immédiatement transférées sur le réseau classique</string>
<string name="yes">Oui</string>
<string name="no">Non</string>
<string name="follow_list_selection">Liste des Abonnés</string>
<string name="follow_list_kind3follows">Tous les abonnements</string>
<string name="follow_list_global">Global</string>
<string name="connect_through_your_orbot_setup_markdown">
## Se connecter à travers Tor avec Orbot
<string name="follow_list_global">Général</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Se connecter à travers Tor avec Orbot
\n\n1. Installe [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Démarre Orbot
\n3. Dans Orbot, vérifie le port Socks. Par défaut 9050
@@ -372,44 +343,33 @@
<string name="invalid_port_number">Numéro de port invalide</string>
<string name="use_orbot">Utiliser Orbot</string>
<string name="disconnect_from_your_orbot_setup">Déconnecter Tor/Orbot</string>
<string name="app_notification_dms_channel_name">Messages privés</string>
<string name="app_notification_dms_channel_description">Vous notifie lorsqu\'un message privé est reçu</string>
<string name="app_notification_zaps_channel_name">Zaps reçus</string>
<string name="app_notification_zaps_channel_description">Vous notifie lorsque quelqu\'un vous zap</string>
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
<string name="app_notification_zaps_channel_message_for">pour %1$s</string>
<string name="reply_notify">Notifier: </string>
<string name="channel_list_join_conversation">Rejoindre Conversation</string>
<string name="channel_list_user_or_group_id">Utilisateur ou Groupe ID</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent ou hex</string>
<string name="channel_list_create_channel">Créer</string>
<string name="channel_list_join_channel">Rejoindre</string>
<string name="today">Aujourd\'hui</string>
<string name="content_warning">Avertissement de contenu</string>
<string name="content_warning_explanation">Ce message contient du contenu sensible que certaines personnes peuvent trouver offensant ou dérangeant</string>
<string name="content_warning_hide_all_sensitive_content">Toujours cacher les contenus sensibles</string>
<string name="content_warning_show_all_sensitive_content">Toujours afficher les contenus sensibles</string>
<string name="content_warning_see_warnings">Toujours afficher les avertissements de contenu</string>
<string name="recommended_apps">Recommendations: </string>
<string name="filter_spam_from_strangers">Filtrer le spam des étrangers</string>
<string name="warn_when_posts_have_reports_from_your_follows">Avertir lorsque les messages ont été rapportés par vos abonnés</string>
<string name="new_reaction_symbol">Nouveau symbole de réaction</string>
<string name="no_reaction_type_setup_long_press_to_change">Aucun type de réaction sélectionné. Appuyer longuement pour changer</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser">Collecte de Zaps</string>
<string name="zapraiser_explainer">Ajoute une quantité cible de sats pour ce message. Les clients compatible peuvent afficher une barre de progrès pour encourager les dons</string>
<string name="zapraiser_target_amount_in_sats">Quantité ciblée en Sats</string>
<string name="sats_to_complete">Zapraiser à %1$s. %2$s sats pour objectif</string>
<string name="read_from_relay">Lire depuis le Relay</string>
<string name="write_to_relay">Écrire sur le Relay</string>
@@ -421,10 +381,10 @@
<string name="supports">NIPs supportés</string>
<string name="admission_fees">Frais d\'admission</string>
<string name="payments_url">URL de payments</string>
<string name="limitations">Limitations</string>
<string name="limitations">Limites</string>
<string name="countries">Pays</string>
<string name="languages">Langages</string>
<string name="tags">Tags</string>
<string name="tags">Étiquettes</string>
<string name="posting_policy">Politique de publication</string>
<string name="message_length">Longueur du message</string>
<string name="subscriptions">Abonnements</string>
@@ -440,34 +400,27 @@
<string name="cashu_redeem">Échanger</string>
<string name="no_lightning_address_set">Adresse Lightning non définie</string>
<string name="copied_token_to_clipboard">Jeton copié dans le presse-papiers</string>
<string name="live_stream_live_tag">EN DIRECT</string>
<string name="live_stream_offline_tag">DÉCONNECTÉ</string>
<string name="live_stream_ended_tag">TERMINÉ</string>
<string name="live_stream_planned_tag">PLANIFIÉ</string>
<string name="live_stream_is_offline">Livestream est déconnecté</string>
<string name="live_stream_has_ended">Livestream terminé</string>
<string name="are_you_sure_you_want_to_log_out">Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ?</string>
<string name="followed_tags">Tags suivis</string>
<string name="relay_setup">Relays</string>
<string name="relay_setup">Relais</string>
<string name="discover_live">Direct</string>
<string name="discover_community">Communauté</string>
<string name="discover_chat">Chats</string>
<string name="discover_chat">Salons</string>
<string name="community_approved_posts">Messages approuvés</string>
<string name="groups_no_descriptor">Ce groupe n\'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une</string>
<string name="community_no_descriptor">Cette communauté n\'a pas de description. Parlez au propriétaire pour en ajouter une</string>
<string name="add_sensitive_content_label">Contenu sensible</string>
<string name="add_sensitive_content_description">Ajouter un avertissement de contenu sensible avant de montrer ce contenu</string>
<string name="settings">Paramètres</string>
<string name="connectivity_type_always">Toujours</string>
<string name="connectivity_type_wifi_only">Wifi uniquement</string>
<string name="connectivity_type_never">Jamais</string>
<string name="system">Système</string>
<string name="light">Clair</string>
<string name="dark">Sombre</string>
@@ -478,62 +431,69 @@
<string name="automatically_play_videos">Lecture vidéo</string>
<string name="automatically_show_url_preview">Prévisualisation des URLs</string>
<string name="load_image">Charger l\'image</string>
<string name="spamming_users">Spammeurs</string>
<string name="muted_button">Silencieux. Cliquer pour réactiver le son</string>
<string name="mute_button">Son activé. Cliquer pour mettre en silencieux</string>
<string name="search_button">Recherche d\'enregistrements locaux et distants</string>
<string name="nip05_verified">L\'adresse Nostr a été vérfiée</string>
<string name="nip05_failed">La vérification de l\'adresse Nostr a échouée</string>
<string name="nip05_checking">Vérification de l\'adresse Nostr</string>
<string name="select_deselect_all">Tout sélectionner/désélectionner</string>
<string name="default_relays">Défaut</string>
<string name="select_a_relay_to_continue">Sélectionner un relay pour continuer</string>
<string name="zap_forward_title">Transférer les Zaps à:</string>
<string name="zap_forward_explainer">Les clients compatibles transmettront des zaps sur l\'adresse LN ou le profil Utilisateur ci-dessous au lieu du vôtre.</string>
<string name="geohash_title">Révéler la localisation comme </string>
<string name="geohash_explainer">Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel</string>
<string name="add_sensitive_content_explainer">Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant</string>
<string name="new_feature_nip24_might_not_be_available_title">Nouvelle Fonctionnalité</string>
<string name="new_feature_nip24_might_not_be_available_description">Pour activer ce mode, Amethyst doit envoyer un message NIP-24 (GiftWrapped, Sealed Direct et Group Messages). Le protocole NIP-24 est nouveau et la plupart des clients ne l\'ont pas encore mis en oeuvre. Assurez-vous que le destinataire utilise un client compatible.</string>
<string name="new_feature_nip24_activate">Activer</string>
<string name="messages_create_public_chat">Public</string>
<string name="messages_new_message">Privé</string>
<string name="messages_new_message_to">À</string>
<string name="messages_new_message_subject">Sujet</string>
<string name="messages_new_message_subject_caption">Sujet de la conversation</string>
<string name="messages_new_message_to_caption">"@Utilisateur1, @Utilisateur2, @Utilisateur3"</string>
<string name="messages_new_message_to_caption">"\@Utilisateur1, @Utilisateur2, @Utilisateur3"</string>
<string name="messages_group_descriptor">Membres de ce groupe</string>
<string name="messages_new_subject_message">Explication aux membres</string>
<string name="messages_new_subject_message_placeholder">Changement de nom pour les nouveaux objectifs.</string>
<string name="language_description">Pour l\'interface de l\'App</string>
<string name="theme_description">Sombre, Clair ou thème Système</string>
<string name="automatically_load_images_gifs_description">Charger automatiquement les images et les GIFs</string>
<string name="automatically_play_videos_description">Lire automatiquement les vidéos et les GIFs</string>
<string name="automatically_show_url_preview_description">Afficher la prévisualisation d\'URL</string>
<string name="load_image_description">Quand charger les images</string>
<string name="copy_url_to_clipboard">Copier l\'URL dans le presse-papiers</string>
<string name="copy_the_note_id_to_the_clipboard">Copier l\'ID de la note dans le presse-papiers</string>
<string name="created_at">Créé le</string>
<string name="rules">Règles</string>
<string name="status_update">Mettez à jour votre statut</string>
<string name="lightning_wallets_not_found">Erreur d\'analyse du message d\'erreur</string>
<string name="poll_zap_value_min_max_explainer">Les votes sont pondérés par le montant du zap. Vous pouvez définir un montant minimum pour éviter les spammeurs et un montant maximum pour éviter qu\'un grand nombre de zappeurs ne prenne le contrôle du sondage. Utilisez le même montant dans les deux champs pour vous assurer que chaque vote a la même valeur. Laissez le champ vide pour accepter n\'importe quel montant.</string>
<string name="error_dialog_zap_error">Impossible d\'envoyer un zap</string>
<string name="error_dialog_talk_to_user">Contacter l\'Utilisateur</string>
<string name="error_dialog_button_ok">Ok</string>
<string name="relay_information_document_error_assemble_url">Impossible d\'atteindre %1$s: %2$s</string>
<string name="relay_information_document_error_reach_server">Impossible d\'atteindre %1$s: %2$s</string>
<string name="relay_information_document_error_parse_result">Impossible d\'analyser le résultat de %1$s: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s a échoué avec le code %2$s</string>
<string name="active_for">Actif pour : </string>
<string name="active_for_home">Accueil</string>
<string name="active_for_msg">MPs</string>
<string name="active_for_chats">Salons</string>
<string name="active_for_global">Général</string>
<string name="active_for_search">Rechercher</string>
<string name="zap_split_title">Diviser et Transférer des Zaps</string>
<string name="zap_split_explainer">Les clients supportés vont diviser et transférer les zaps aux utilisateurs ajoutés ici au lieu du vôtre</string>
<string name="zap_split_serarch_and_add_user">Rechercher et Ajouter un Utilisateur</string>
<string name="zap_split_serarch_and_add_user_placeholder">Nom d\'utilisateur ou nom affiché</string>
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur %1$s n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
<string name="zap_split_weight">Pourcentage</string>
<string name="zap_split_weight_placeholder">25</string>
<string name="splitting_zaps_with">Diviser les zaps avec</string>
<string name="forwarding_zaps_to">Transférer les zaps à</string>
<string name="lightning_wallets_not_found2">Portefeuilles Lightning introuvables</string>
<string name="paid">Payé</string>
<string name="wallet_number">Portefeuille %1$s</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>

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