Compare commits

...

156 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
Vitor Pamplona
c69969947b v0.75.8: Fixes status's cache update crash. 2023-08-27 19:33:08 -04:00
Vitor Pamplona
e1738a25a1 Do a warning log and not an error log for verification fails (it usually happens in stringified JSON inside .content) 2023-08-27 19:17:58 -04:00
Vitor Pamplona
6c54082a12 clears the index cache when changing status fields. 2023-08-27 19:16:58 -04:00
Vitor Pamplona
bc5500731d v0.75.7 2023-08-27 18:12:41 -04:00
Vitor Pamplona
0665c552aa BugFix for a crash when receiving a new notification 2023-08-27 18:11:14 -04:00
Vitor Pamplona
1ab8463578 v0.75.6 2023-08-27 17:13:29 -04:00
Vitor Pamplona
e016e68e42 removing unused sentence. 2023-08-27 17:11:49 -04:00
Vitor Pamplona
d116159ed9 v0.75.5 2023-08-27 16:49:31 -04:00
Vitor Pamplona
3c24c011e3 Avoids rebuilding the channel object in DataSource. 2023-08-27 16:45:12 -04:00
Vitor Pamplona
536bdfe43a - When All Follows, the live tab will load any event with a follow in the participant list.
- Removes prefix filters
2023-08-27 16:32:35 -04:00
Vitor Pamplona
0b41110938 Fixes formatting 2023-08-27 15:53:41 -04:00
Vitor Pamplona
da1d4018cf Fixes crash when more than 1 status 2023-08-27 15:52:35 -04:00
Vitor Pamplona
b569c67d3b Fixes Hungarian labels 2023-08-27 15:33:18 -04:00
Vitor Pamplona
50a5a68b71 Merge pull request #554 from anthony-robin/translations
Update french translations
2023-08-27 15:30:42 -04:00
Vitor Pamplona
23a4d80ddb v0.75.4 2023-08-27 12:25:53 -04:00
Vitor Pamplona
6a64ebcc5b migrates to a double layer observer structure for the memory cache 2023-08-27 12:18:11 -04:00
Vitor Pamplona
8e622bfbb1 Updates to debugging logs 2023-08-27 12:16:36 -04:00
Vitor Pamplona
d189dce7f0 Extracts chatroom from user file 2023-08-27 12:16:13 -04:00
Vitor Pamplona
43ac1bf555 Allows cleaning up on App Pause to perform separately from memory pruning functions 2023-08-27 12:10:12 -04:00
Anthony Robin
d2d26e4e57 Update french translations
Update french translations according to newly added english ones.
2023-08-27 07:52:15 +02:00
Vitor Pamplona
de1f13d64c Faster crossfade animation between screens 2023-08-26 13:04:35 -04:00
Vitor Pamplona
2be04bfbe2 Better error handling when Zaps fail 2023-08-26 13:00:53 -04:00
Vitor Pamplona
57c33ae06a Improves User filters by merging lastEOSEs into a single filter. 2023-08-26 12:59:41 -04:00
Vitor Pamplona
99e545f0b8 Adds polls to the hashtag screen 2023-08-26 11:32:37 -04:00
Vitor Pamplona
accb115a20 Making statuses filter just one with the minimum of all observing users. 2023-08-26 10:36:44 -04:00
Vitor Pamplona
9de1cec160 Merge pull request #553 from ZsZolee/main
Additional Hungarian translation
2023-08-26 09:34:36 -04:00
Zoltan
f7774700db Additional Hungarian translation 2023-08-26 13:22:27 +02:00
Vitor Pamplona
ffb81c6246 v0.75.3 2023-08-25 17:51:38 -04:00
Vitor Pamplona
b7c9b14b1f Allowing any BaseTextNote on the reply window 2023-08-25 17:47:01 -04:00
Vitor Pamplona
53b0db61ea Moves isAcceptableNote calculations to viewModel 2023-08-25 17:32:19 -04:00
Vitor Pamplona
6c09e47e4f Moves coroutine of NIP-05 verification to the viewModel 2023-08-25 17:26:47 -04:00
Vitor Pamplona
9d9ad63b4d BugFix deleting status 2023-08-25 17:15:17 -04:00
Vitor Pamplona
7ef46a2662 Continues to move away from starting co-routines directly in composables 2023-08-25 17:14:33 -04:00
Vitor Pamplona
a7d8c87c47 1. Triggers a Toast when no wallet is found to pay the invoice
2. Starts the wallet in a separate task to allow people to come back to Amethyst switching apps instead of the back button.
2023-08-25 15:18:01 -04:00
Vitor Pamplona
1bb2459277 Activates min/max values for polls 2023-08-25 15:08:10 -04:00
Vitor Pamplona
6b39562925 Allows the keyboard action to be used as commit in the status field. 2023-08-25 13:55:41 -04:00
Vitor Pamplona
3d34e1fc4e Reduces the icon to the music and adds space before the status 2023-08-25 13:41:17 -04:00
Vitor Pamplona
469e1241d5 Updates some dependencies 2023-08-25 13:28:17 -04:00
Vitor Pamplona
86c0ca975b Moves the User DataSource invalidation to the right channel 2023-08-25 13:27:57 -04:00
Vitor Pamplona
53eeea3225 Removes unused time 2023-08-25 13:26:56 -04:00
Vitor Pamplona
e9ff9d3b37 Adds a space between the status and the clickable icon 2023-08-25 13:24:47 -04:00
Vitor Pamplona
e2c011ec60 Avoids lists with the same hexId because the app was pruning events while loading them 2023-08-25 13:01:49 -04:00
Vitor Pamplona
5328740bff Improves blinking when loading a thread. 2023-08-25 13:01:08 -04:00
Vitor Pamplona
017df9845d Fixes r filter in tags 2023-08-25 12:59:21 -04:00
Vitor Pamplona
ed70658be9 Removes unecessary @ExperimentalTime tag 2023-08-25 11:52:44 -04:00
Vitor Pamplona
dc74c3014f updates cached content when note changes 2023-08-25 11:51:41 -04:00
Vitor Pamplona
53d5c5f354 BugFix for serialization filter 2023-08-25 11:49:41 -04:00
Vitor Pamplona
63ab2a9fbe Moves NIP315 to NIP38 2023-08-25 10:31:51 -04:00
154 changed files with 6482 additions and 3180 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

@@ -83,7 +83,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Gift Wraps & Seals (NIP-59)
- [x] Versioned Encrypted Payloads (NIP-44)
- [x] Expiration Support (NIP-40)
- [x] Status Event (NIP-315)
- [x] Status Event (NIP-38)
- [ ] Marketplace (NIP-15)
- [ ] Image/Video Capture in the app
- [ ] Local Database

View File

@@ -13,14 +13,14 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 281
versionName "0.75.2"
versionCode 294
versionName "0.76.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
}
}
@@ -171,7 +171,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.2.2')
playImplementation platform('com.google.firebase:firebase-bom:32.2.3')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts

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

@@ -115,7 +115,11 @@ object ServiceManager {
Client.disconnect()
}
fun cleanUp() {
fun cleanObservers() {
LocalCache.cleanObservers()
}
fun trimMemory() {
LocalCache.cleanObservers()
val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet()

View File

@@ -101,14 +101,16 @@ class Account(
val showSensitiveContent: Boolean?
)
val liveHiddenUsers: LiveData<LiveHiddenUsers> = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}.distinctUntilChanged()
val liveHiddenUsers: LiveData<LiveHiddenUsers> by lazy {
live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}.distinctUntilChanged()
}
var userProfileCache: User? = null
@@ -1027,6 +1029,21 @@ class Account(
LocalCache.consume(event, null)
}
fun deleteStatus(oldStatus: AddressableNote) {
if (!isWriteable()) return
val oldEvent = oldStatus.event as? StatusEvent ?: return
val event = StatusEvent.clear(oldEvent, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event, null)
val event2 = DeletionEvent.create(listOf(event.id), keyPair.privKey!!)
Client.send(event2)
LocalCache.consume(event2)
}
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
if (!isWriteable()) return
@@ -1644,14 +1661,18 @@ class Account(
// Ugly, but forces nostr.band as the only search-supporting relay today.
// TODO: Remove when search becomes more available.
if (usersRelayList.none { it.activeTypes.contains(FeedType.SEARCH) }) {
usersRelayList = usersRelayList + Relay(
Constants.forcedRelayForSearch.url,
Constants.forcedRelayForSearch.read,
Constants.forcedRelayForSearch.write,
Constants.forcedRelayForSearch.feedTypes,
proxy
)
val searchRelays = usersRelayList.filter { it.url.removeSuffix("/") in Constants.forcedRelaysForSearchSet }
val hasSearchRelay = usersRelayList.any { it.activeTypes.contains(FeedType.SEARCH) }
if (!hasSearchRelay && searchRelays.isEmpty()) {
usersRelayList = usersRelayList + Constants.forcedRelayForSearch.map {
Relay(
it.url,
it.read,
it.write,
it.feedTypes,
proxy
)
}
}
return usersRelayList.toTypedArray()
@@ -1669,6 +1690,10 @@ class Account(
.toTypedArray()
}
fun activeWriteRelays(): List<Relay> {
return (activeRelays() ?: convertLocalRelays()).filter { it.write }
}
fun reconnectIfRelaysHaveChanged() {
val newRelaySet = activeRelays() ?: convertLocalRelays()
if (!Client.isSameRelaySetConfig(newRelaySet)) {

View File

@@ -164,20 +164,12 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
override fun onActive() {
super.onActive()
if (channel is PublicChatChannel) {
NostrSingleChannelDataSource.add(channel.idHex)
} else {
NostrSingleChannelDataSource.add(channel.idHex)
}
NostrSingleChannelDataSource.add(channel)
}
override fun onInactive() {
super.onInactive()
if (channel is PublicChatChannel) {
NostrSingleChannelDataSource.remove(channel.idHex)
} else {
NostrSingleChannelDataSource.remove(channel.idHex)
}
NostrSingleChannelDataSource.remove(channel)
}
}

View File

@@ -0,0 +1,63 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.utils.TimeUtils
@Stable
class Chatroom() {
var roomMessages: Set<Note> = setOf()
var subject: String? = null
var subjectCreatedAt: Long? = null
@Synchronized
fun addMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
val newSubject = msg.event?.subject()
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
subject = newSubject
subjectCreatedAt = msg.createdAt()
}
}
}
@Synchronized
fun removeMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let {
subject = it.event?.subject()
subjectCreatedAt = it.createdAt()
}
}
}
fun senderIntersects(keySet: Set<HexKey>): Boolean {
return roomMessages.any { it.author?.pubkeyHex in keySet }
}
fun pruneMessagesToTheLatestOnly(): Set<Note> {
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
// Recent messages, keep last 100
sorted.take(100).toSet()
} else {
// Old messages, keep the last one.
sorted.take(1).toSet()
} + sorted.filter { it.liveSet?.isInUse() ?: false }
val toRemove = roomMessages.minus(toKeep)
roomMessages = toKeep
return toRemove
}
}

View File

@@ -14,7 +14,6 @@ import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
@@ -361,7 +360,7 @@ object LocalCache {
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
author.liveSet?.statuses?.invalidateData()
author.liveSet?.innerStatuses?.invalidateData()
refreshObservers(note)
}
@@ -651,7 +650,7 @@ object LocalCache {
mentions.forEach {
// doesn't add to reports, but triggers recounts
it.liveSet?.reports?.invalidateData()
it.liveSet?.innerReports?.invalidateData()
}
}
@@ -1184,13 +1183,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())
}
@@ -1209,12 +1202,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())
}
@@ -1241,21 +1229,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())
}
@@ -1275,6 +1254,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
@@ -1283,32 +1263,91 @@ 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()
val now = TimeUtils.now()
val toBeRemoved = notes.filter {
it.value.event?.isExpired() == true
}.values
@@ -1316,17 +1355,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())
}
@@ -1353,17 +1382,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())
}
@@ -1372,21 +1391,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()
@@ -1479,9 +1483,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

@@ -3,8 +3,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
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.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
@@ -22,6 +22,7 @@ import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import java.math.BigDecimal
import java.time.Instant
@@ -126,7 +127,7 @@ open class Note(val idHex: String) {
this.author = author
this.replyTo = replyTo
liveSet?.metadata?.invalidateData()
liveSet?.innerMetadata?.invalidateData()
}
}
@@ -135,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
@@ -174,21 +209,21 @@ open class Note(val idHex: String) {
fun addReply(note: Note) {
if (note !in replies) {
replies = replies + note
liveSet?.replies?.invalidateData()
liveSet?.innerReplies?.invalidateData()
}
}
fun removeReply(note: Note) {
if (note in replies) {
replies = replies - note
liveSet?.replies?.invalidateData()
liveSet?.innerReplies?.invalidateData()
}
}
fun removeBoost(note: Note) {
if (note in boosts) {
boosts = boosts - note
liveSet?.boosts?.invalidateData()
liveSet?.innerBoosts?.invalidateData()
}
}
@@ -211,11 +246,11 @@ open class Note(val idHex: String) {
relays = listOf<String>()
lastReactionsDownloadTime = emptyMap()
liveSet?.replies?.invalidateData()
liveSet?.reactions?.invalidateData()
liveSet?.boosts?.invalidateData()
liveSet?.reports?.invalidateData()
liveSet?.zaps?.invalidateData()
liveSet?.innerReplies?.invalidateData()
liveSet?.innerReactions?.invalidateData()
liveSet?.innerBoosts?.invalidateData()
liveSet?.innerReports?.invalidateData()
liveSet?.innerZaps?.invalidateData()
return toBeRemoved
}
@@ -234,7 +269,7 @@ open class Note(val idHex: String) {
reactions = reactions + Pair(reaction, newList)
}
liveSet?.reactions?.invalidateData()
liveSet?.innerReactions?.invalidateData()
}
}
}
@@ -246,7 +281,7 @@ open class Note(val idHex: String) {
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
}
}
}
@@ -254,28 +289,28 @@ open class Note(val idHex: String) {
fun removeZap(note: Note) {
if (zaps[note] != null) {
zaps = zaps.minus(note)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
} else if (zaps.containsValue(note)) {
zaps = zaps.filterValues { it != note }
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
fun removeZapPayment(note: Note) {
if (zapPayments[note] != null) {
zapPayments = zapPayments.minus(note)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
} else if (zapPayments.containsValue(note)) {
val toRemove = zapPayments.filterValues { it == note }
zapPayments = zapPayments.minus(toRemove.keys)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
fun addBoost(note: Note) {
if (note !in boosts) {
boosts = boosts + note
liveSet?.boosts?.invalidateData()
liveSet?.innerBoosts?.invalidateData()
}
}
@@ -297,12 +332,12 @@ open class Note(val idHex: String) {
if (zapRequest !in zaps.keys) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
} else if (zaps[zapRequest] == null) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
}
@@ -325,12 +360,12 @@ open class Note(val idHex: String) {
if (zapPaymentRequest !in zapPayments.keys) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
} else if (zapPayments[zapPaymentRequest] == null) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
}
@@ -341,10 +376,10 @@ open class Note(val idHex: String) {
if (reaction !in reactions.keys) {
reactions = reactions + Pair(reaction, listOf(note))
liveSet?.reactions?.invalidateData()
liveSet?.innerReactions?.invalidateData()
} else if (reactions[reaction]?.contains(note) == false) {
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
liveSet?.reactions?.invalidateData()
liveSet?.innerReactions?.invalidateData()
}
}
@@ -353,17 +388,17 @@ open class Note(val idHex: String) {
if (author !in reports.keys) {
reports = reports + Pair(author, listOf(note))
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
} else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
}
}
fun addRelay(relay: Relay) {
if (relay.url !in relays) {
relays = relays + relay.url
liveSet?.relays?.invalidateData()
liveSet?.innerRelays?.invalidateData()
}
}
@@ -463,10 +498,6 @@ open class Note(val idHex: String) {
}.flatten()
}
fun countReactions(): Int {
return reactions.values.sumOf { it.size }
}
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts
val completedZaps = zaps.asSequence()
@@ -657,36 +688,58 @@ open class Note(val idHex: String) {
@Stable
class NoteLiveSet(u: Note) {
// Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u)
val innerMetadata = NoteBundledRefresherLiveData(u)
val innerReactions = NoteBundledRefresherLiveData(u)
val innerBoosts = NoteBundledRefresherLiveData(u)
val innerReplies = NoteBundledRefresherLiveData(u)
val innerReports = NoteBundledRefresherLiveData(u)
val innerRelays = NoteBundledRefresherLiveData(u)
val innerZaps = NoteBundledRefresherLiveData(u)
val authorChanges = metadata.map {
val metadata = innerMetadata.map { it }
val reactions = innerReactions.map { it }
val boosts = innerBoosts.map { it }
val replies = innerReplies.map { it }
val reports = innerReports.map { it }
val relays = innerRelays.map { it }
val zaps = innerZaps.map { it }
val authorChanges = innerMetadata.map {
it.note.author
}
val hasEvent = metadata.map {
val hasEvent = innerMetadata.map {
it.note.event != null
}.distinctUntilChanged()
val reactions: NoteLiveData = NoteLiveData(u)
val boosts: NoteLiveData = NoteLiveData(u)
val replies: NoteLiveData = NoteLiveData(u)
val reports: NoteLiveData = NoteLiveData(u)
val relays: NoteLiveData = NoteLiveData(u)
val zaps: NoteLiveData = NoteLiveData(u)
val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState ->
val hasReactions = innerZaps.combineWith(innerBoosts, innerReactions) { zapState, boostState, reactionState ->
zapState?.note?.zaps?.isNotEmpty() ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}.distinctUntilChanged()
val replyCount = replies.map {
val replyCount = innerReplies.map {
it.note.replies.size
}.distinctUntilChanged()
val boostCount = boosts.map {
val reactionCount = innerReactions.map {
it.note.reactions.values.sumOf { it.size }
}.distinctUntilChanged()
val boostCount = innerBoosts.map {
it.note.boosts.size
}.distinctUntilChanged()
val boostList = innerBoosts.map {
it.note.boosts.toImmutableList()
}.distinctUntilChanged()
val relayInfo = innerRelays.map {
it.note.relays.map {
RelayBriefInfo(it)
}.toImmutableList()
}
fun isInUse(): Boolean {
return metadata.hasObservers() ||
reactions.hasObservers() ||
@@ -694,21 +747,28 @@ class NoteLiveSet(u: Note) {
replies.hasObservers() ||
reports.hasObservers() ||
relays.hasObservers() ||
zaps.hasObservers()
zaps.hasObservers() ||
authorChanges.hasObservers() ||
hasEvent.hasObservers() ||
hasReactions.hasObservers() ||
replyCount.hasObservers() ||
reactionCount.hasObservers() ||
boostCount.hasObservers() ||
boostList.hasObservers()
}
fun destroy() {
metadata.destroy()
reactions.destroy()
boosts.destroy()
replies.destroy()
reports.destroy()
relays.destroy()
zaps.destroy()
innerMetadata.destroy()
innerReactions.destroy()
innerBoosts.destroy()
innerReplies.destroy()
innerReports.destroy()
innerRelays.destroy()
innerZaps.destroy()
}
}
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
@@ -717,19 +777,26 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
}
fun invalidateData() {
if (!hasObservers()) return
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(NoteState(note))
}
postValue(NoteState(note))
}
}
fun <Y> map(
transform: (NoteState) -> Y
): NoteLoadingLiveData<Y> {
val initialValue = this.value?.let { transform(it) }
val result = NoteLoadingLiveData(note, initialValue)
result.addSource(this) { x -> result.value = transform(x) }
return result
}
}
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
override fun onActive() {
super.onActive()
if (note is AddressableNote) {
@@ -751,3 +818,10 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
@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

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
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
@@ -23,7 +24,6 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import java.math.BigDecimal
@@ -101,7 +101,11 @@ class User(val pubkeyHex: String) {
if (event.id == latestBookmarkList?.id) return
latestBookmarkList = event
liveSet?.bookmarks?.invalidateData()
liveSet?.innerBookmarks?.invalidateData()
}
fun clearEOSE() {
latestEOSEs = emptyMap()
}
fun updateContactList(event: ContactListEvent) {
@@ -111,18 +115,18 @@ class User(val pubkeyHex: String) {
latestContactList = event
// Update following of the current user
liveSet?.follows?.invalidateData()
liveSet?.innerFollows?.invalidateData()
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
}
liveSet?.relays?.invalidateData()
liveSet?.innerRelays?.invalidateData()
}
fun addReport(note: Note) {
@@ -130,10 +134,10 @@ class User(val pubkeyHex: String) {
if (author !in reports.keys) {
reports = reports + Pair(author, setOf(note))
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
} else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
}
}
@@ -143,7 +147,7 @@ class User(val pubkeyHex: String) {
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.reports?.invalidateData()
liveSet?.innerReports?.invalidateData()
}
}
}
@@ -151,20 +155,20 @@ class User(val pubkeyHex: String) {
fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
fun removeZap(zapRequestOrZapEvent: Note) {
if (zapRequestOrZapEvent in zaps.keys) {
zaps = zaps.minus(zapRequestOrZapEvent)
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
} else if (zapRequestOrZapEvent in zaps.values) {
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
liveSet?.zaps?.invalidateData()
liveSet?.innerZaps?.invalidateData()
}
}
@@ -218,7 +222,7 @@ class User(val pubkeyHex: String) {
val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg)
liveSet?.messages?.invalidateData()
liveSet?.innerMessages?.invalidateData()
}
}
@@ -226,7 +230,7 @@ class User(val pubkeyHex: String) {
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg)
liveSet?.messages?.invalidateData()
liveSet?.innerMessages?.invalidateData()
}
}
@@ -240,7 +244,7 @@ class User(val pubkeyHex: String) {
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg)
liveSet?.messages?.invalidateData()
liveSet?.innerMessages?.invalidateData()
}
}
@@ -255,7 +259,7 @@ class User(val pubkeyHex: String) {
here.counter++
}
liveSet?.relayInfo?.invalidateData()
liveSet?.innerRelayInfo?.invalidateData()
}
fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) {
@@ -280,7 +284,7 @@ class User(val pubkeyHex: String) {
}
}
liveSet?.metadata?.invalidateData()
liveSet?.innerMetadata?.invalidateData()
}
fun isFollowing(user: User): Boolean {
@@ -382,54 +386,70 @@ class User(val pubkeyHex: String) {
@Stable
class UserLiveSet(u: User) {
// UI Observers line up here.
val follows: UserLiveData = UserLiveData(u)
val followers: UserLiveData = UserLiveData(u)
val reports: UserLiveData = UserLiveData(u)
val messages: UserLiveData = UserLiveData(u)
val relays: UserLiveData = UserLiveData(u)
val relayInfo: UserLiveData = UserLiveData(u)
val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u)
val statuses: UserLiveData = UserLiveData(u)
val innerMetadata = UserBundledRefresherLiveData(u)
val profilePictureChanges = metadata.map {
// UI Observers line up here.
val innerFollows = UserBundledRefresherLiveData(u)
val innerFollowers = UserBundledRefresherLiveData(u)
val innerReports = UserBundledRefresherLiveData(u)
val innerMessages = UserBundledRefresherLiveData(u)
val innerRelays = UserBundledRefresherLiveData(u)
val innerRelayInfo = UserBundledRefresherLiveData(u)
val innerZaps = UserBundledRefresherLiveData(u)
val innerBookmarks = UserBundledRefresherLiveData(u)
val innerStatuses = UserBundledRefresherLiveData(u)
// UI Observers line up here.
val metadata = innerMetadata.map { it }
val follows = innerFollows.map { it }
val followers = innerFollowers.map { it }
val reports = innerReports.map { it }
val messages = innerMessages.map { it }
val relays = innerRelays.map { it }
val relayInfo = innerRelayInfo.map { it }
val zaps = innerZaps.map { it }
val bookmarks = innerBookmarks.map { it }
val statuses = innerStatuses.map { it }
val profilePictureChanges = innerMetadata.map {
it.user.profilePicture()
}.distinctUntilChanged()
val nip05Changes = metadata.map {
val nip05Changes = innerMetadata.map {
it.user.nip05()
}.distinctUntilChanged()
val userMetadataInfo = metadata.map {
val userMetadataInfo = innerMetadata.map {
it.user.info
}.distinctUntilChanged()
fun isInUse(): Boolean {
return follows.hasObservers() ||
return metadata.hasObservers() ||
follows.hasObservers() ||
followers.hasObservers() ||
reports.hasObservers() ||
messages.hasObservers() ||
relays.hasObservers() ||
relayInfo.hasObservers() ||
metadata.hasObservers() ||
zaps.hasObservers() ||
bookmarks.hasObservers() ||
statuses.hasObservers() ||
bookmarks.hasObservers()
profilePictureChanges.hasObservers() ||
nip05Changes.hasObservers() ||
userMetadataInfo.hasObservers()
}
fun destroy() {
follows.destroy()
followers.destroy()
reports.destroy()
messages.destroy()
relays.destroy()
relayInfo.destroy()
metadata.destroy()
zaps.destroy()
bookmarks.destroy()
statuses.destroy()
innerMetadata.destroy()
innerFollows.destroy()
innerFollowers.destroy()
innerReports.destroy()
innerMessages.destroy()
innerRelays.destroy()
innerRelayInfo.destroy()
innerZaps.destroy()
innerBookmarks.destroy()
innerStatuses.destroy()
}
}
@@ -440,64 +460,7 @@ data class RelayInfo(
var counter: Long
)
@Stable
class Chatroom() {
var roomMessages: Set<Note> = setOf()
var subject: String? = null
var subjectCreatedAt: Long? = null
@Synchronized
fun addMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
val newSubject = msg.event?.subject()
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
subject = newSubject
subjectCreatedAt = msg.createdAt()
}
}
}
@Synchronized
fun removeMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let {
subject = it.event?.subject()
subjectCreatedAt = it.createdAt()
}
}
}
fun senderIntersects(keySet: Set<HexKey>): Boolean {
return roomMessages.any { it.author?.pubkeyHex in keySet }
}
fun pruneMessagesToTheLatestOnly(): Set<Note> {
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
// Recent messages, keep last 100
sorted.take(100).toSet()
} else {
// Old messages, keep the last one.
sorted.take(1).toSet()
} + sorted.filter { it.liveSet?.isInUse() ?: false }
val toRemove = roomMessages.minus(toKeep)
roomMessages = toKeep
return toRemove
}
}
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
@@ -506,18 +469,26 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
}
fun invalidateData() {
if (!hasObservers()) return
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(UserState(user))
}
postValue(UserState(user))
}
}
fun <Y> map(
transform: (UserState) -> Y
): UserLoadingLiveData<Y> {
val initialValue = this.value?.let { transform(it) }
val result = UserLoadingLiveData(user, initialValue)
result.addSource(this) { x -> result.value = transform(x) }
return result
}
}
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
override fun onActive() {
super.onActive()
NostrSingleUserDataSource.add(user)

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

@@ -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) {
@@ -111,12 +119,14 @@ abstract class NostrDataSource(val debugName: String) {
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateFilters() {
bundler.invalidate() {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
scope.launch(Dispatchers.IO) {
bundler.invalidate() {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
resetFiltersSuspend()
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
resetFiltersSuspend()
}
}
}
@@ -134,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.
@@ -165,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

@@ -18,53 +18,66 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
val latestEOSEs = EOSEAccount()
fun createLiveStreamFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
fun createLiveStreamFilter(): List<TypedFilter> {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
val followKeys = follows?.map {
it.substring(0, 8)
}
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = follows,
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
),
follows?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
tags = mapOf("p" to it),
kinds = listOf(LiveActivitiesEvent.kind),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
)
}
fun createPublicChatFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
fun createPublicChatFilter(): List<TypedFilter> {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
val followChats = account.selectedChatsFollowList().toList()
val followKeys = follows?.map {
it.substring(0, 8)
}
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
return listOf(
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
authors = follows,
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
),
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
ids = followChats,
kinds = listOf(ChannelCreateEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
)
}
fun createCommunitiesFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
val followKeys = follows?.map {
it.substring(0, 8)
}
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
authors = follows,
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
@@ -197,16 +210,16 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters = listOfNotNull(
createLiveStreamFilter(),
createPublicChatFilter(),
createCommunitiesFilter(),
createLiveStreamTagsFilter(),
createPublicChatsTagsFilter(),
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsGeohashesFilter(),
createLiveStreamGeohashesFilter()
discoveryFeedChannel.typedFilters = createLiveStreamFilter().plus(createPublicChatFilter()).plus(
listOfNotNull(
createLiveStreamTagsFilter(),
createLiveStreamGeohashesFilter(),
createCommunitiesFilter(),
createPublicChatsTagsFilter(),
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsGeohashesFilter()
)
).ifEmpty { null }
}
}

View File

@@ -55,12 +55,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
fun createFollowAccountsFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
val followKeys = follows?.map {
it.substring(0, 8)
}
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 8))
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
return TypedFilter(
types = setOf(FeedType.FOLLOWS),

View File

@@ -1,7 +1,7 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
@@ -11,10 +11,10 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<String>()
private var channelsToWatch = setOf<Channel>()
private fun createRepliesAndReactionsFilter(): TypedFilter? {
val reactionsToWatch = channelsToWatch.map { it }
private fun createMetadataChangeFilter(): TypedFilter? {
val reactionsToWatch = channelsToWatch.filter { it is PublicChatChannel }.map { it.idHex }
if (reactionsToWatch.isEmpty()) {
return null
@@ -32,7 +32,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val directEventsToLoad = channelsToWatch
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
.filter { it.notes.isEmpty() && it is PublicChatChannel }
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
@@ -53,7 +52,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad = channelsToWatch
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
.filterIsInstance<LiveActivitiesChannel>()
.filter { it.info == null }
@@ -81,7 +79,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
val singleChannelChannel = requestNewChannel()
override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter()
val reactions = createMetadataChangeFilter()
val missing = createLoadEventsIfNotLoadedFilter()
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
@@ -90,13 +88,17 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
).ifEmpty { null }
}
fun add(eventId: String) {
channelsToWatch = channelsToWatch.plus(eventId)
invalidateFilters()
fun add(eventId: Channel) {
if (eventId !in channelsToWatch) {
channelsToWatch = channelsToWatch.plus(eventId)
invalidateFilters()
}
}
fun remove(eventId: String) {
channelsToWatch = channelsToWatch.minus(eventId)
invalidateFilters()
fun remove(eventId: Channel) {
if (eventId in channelsToWatch) {
channelsToWatch = channelsToWatch.minus(eventId)
invalidateFilters()
}
}
}

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

@@ -12,49 +12,60 @@ import com.vitorpamplona.quartz.events.StatusEvent
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<User>()
fun createUserFilter(): List<TypedFilter>? {
fun createUserMetadataFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
return usersToWatch.filter { it.info?.latestMetadata == null }.map {
val firstTimers = usersToWatch.filter { it.info?.latestMetadata == null }.map { it.pubkeyHex }
return listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex),
limit = 1
kinds = listOf(MetadataEvent.kind, StatusEvent.kind),
authors = firstTimers,
limit = 10 * firstTimers.size
)
)
}
)
}
fun createUserStatusFilter(): List<TypedFilter>? {
fun createUserMetadataFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
if (usersToWatch.isEmpty()) return null
return usersToWatch.map {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(StatusEvent.kind),
authors = listOf(it.pubkeyHex),
since = it.latestEOSEs
)
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(StatusEvent.kind),
authors = usersToWatch.map { it.pubkeyHex },
since = minLatestEOSEs
)
}
)
}
fun createUserReportFilter(): List<TypedFilter>? {
fun createUserStatusFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
if (usersToWatch.isEmpty()) return null
return usersToWatch.map {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
since = it.latestEOSEs
)
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(StatusEvent.kind),
authors = usersToWatch.map { it.pubkeyHex },
since = minLatestEOSEs
)
}
)
}
fun createUserReportFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
if (usersToWatch.isEmpty()) return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
tags = mapOf("p" to usersToWatch.map { it.pubkeyHex }),
since = minLatestEOSEs
)
)
}
val userChannel = requestNewChannel() { time, relayUrl ->
@@ -66,17 +77,40 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
eose.time = time
}
}
}
val userChannelFirstTimers = requestNewChannel() { time, relayUrl ->
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()
}
val userChannelOnce = requestNewChannel()
override fun updateChannelFilters() {
userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null }
userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
val neverGottenAnEOSE = mutableSetOf<String>()
usersToWatch.forEach {
if (it.latestEOSEs.isEmpty()) { // first time
neverGottenAnEOSE.add(it.pubkeyHex)
} else {
it.latestEOSEs.forEach {
val minEose = minLatestEOSEs[it.key]
if (minEose == null) {
minLatestEOSEs.put(it.key, EOSETime(it.value.time))
} else if (it.value.time < minEose.time) {
minEose.time = it.value.time
}
}
}
}
userChannel.typedFilters = listOfNotNull(
createUserMetadataFilter(minLatestEOSEs),
createUserStatusFilter(minLatestEOSEs),
createUserReportFilter(minLatestEOSEs)
).ifEmpty { null }
userChannelFirstTimers.typedFilters = listOfNotNull(
createUserMetadataFilter()
).flatten().ifEmpty { null }
}
fun add(user: User) {

View File

@@ -36,8 +36,10 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
}
fun loadThread(noteId: String?) {
eventToWatch = noteId
if (eventToWatch != noteId) {
eventToWatch = noteId
invalidateFilters()
invalidateFilters()
}
}
}

View File

@@ -14,16 +14,12 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
val latestEOSEs = EOSEAccount()
fun createContextualFilter(): TypedFilter? {
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
val followKeys = follows?.map {
it.substring(0, 6)
}
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
authors = follows,
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
limit = 200,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList

View File

@@ -57,12 +57,12 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress 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")
}
}
@@ -164,9 +164,16 @@ class LightningAddressResolver() {
onProgress(0.7f)
onSuccess(pr)
} else {
onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server")
}
} ?: 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

@@ -19,28 +19,46 @@ 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
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) {
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 ?: return
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)
}
}
}
}
fun unwrapAndConsume(event: Event, account: Account): Event? {
private fun unwrapAndConsume(event: Event, account: Account): Event? {
if (!LocalCache.justVerify(event)) return null
return when (event) {
@@ -65,106 +83,106 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
private 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 != 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)
}
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

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

@@ -60,7 +60,7 @@ class JsonFilter(
} else {
val jsonObjectSince = factory.objectNode()
entries.forEach { sincePairs ->
put(sincePairs.key, "${sincePairs.value}")
jsonObjectSince.put(sincePairs.key, "${sincePairs.value}")
}
put("since", jsonObjectSince)
}

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
@@ -65,7 +66,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 +85,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)
}
@@ -103,6 +108,7 @@ class MainActivity : AppCompatActivity() {
}
override fun onPause() {
ServiceManager.cleanObservers()
// if (BuildConfig.DEBUG) {
debugState(this)
// }
@@ -127,7 +133,7 @@ class MainActivity : AppCompatActivity() {
super.onTrimMemory(level)
println("Trim Memory $level")
GlobalScope.launch(Dispatchers.Default) {
ServiceManager.cleanUp()
ServiceManager.trimMemory()
}
}
@@ -152,7 +158,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")
@@ -177,9 +184,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

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

@@ -1,9 +1,11 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
@@ -11,8 +13,6 @@ import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -21,51 +21,11 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
var textMax by rememberSaveable { mutableStateOf("") }
var textMin by rememberSaveable { mutableStateOf("") }
// check for zapMax amounts < 1
pollViewModel.isValidvalueMaximum.value = true
if (textMax.isNotEmpty()) {
try {
val int = textMax.toInt()
if (int < 1) {
pollViewModel.isValidvalueMaximum.value = false
} else { pollViewModel.valueMaximum = int }
} catch (e: Exception) { pollViewModel.isValidvalueMaximum.value = false }
}
// check for minZap amounts < 1
pollViewModel.isValidvalueMinimum.value = true
if (textMin.isNotEmpty()) {
try {
val int = textMin.toInt()
if (int < 1) {
pollViewModel.isValidvalueMinimum.value = false
} else { pollViewModel.valueMinimum = int }
} catch (e: Exception) { pollViewModel.isValidvalueMinimum.value = false }
}
// check for zapMin > zapMax
if (textMin.isNotEmpty() && textMax.isNotEmpty()) {
try {
val intMin = textMin.toInt()
val intMax = textMax.toInt()
if (intMin > intMax) {
pollViewModel.isValidvalueMinimum.value = false
pollViewModel.isValidvalueMaximum.value = false
}
} catch (e: Exception) {
pollViewModel.isValidvalueMinimum.value = false
pollViewModel.isValidvalueMaximum.value = false
}
}
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
unfocusedBorderColor = Color.Red
@@ -80,10 +40,10 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
horizontalArrangement = Arrangement.Center
) {
OutlinedTextField(
value = textMin,
onValueChange = { textMin = it },
value = pollViewModel.valueMinimum?.toString() ?: "",
onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.width(150.dp),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid,
label = {
Text(
@@ -98,11 +58,14 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
)
}
)
Spacer(modifier = DoubleHorzSpacer)
OutlinedTextField(
value = textMax,
onValueChange = { textMax = it },
value = pollViewModel.valueMaximum?.toString() ?: "",
onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.width(150.dp),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid,
label = {
Text(
@@ -118,10 +81,25 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
}
)
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.poll_zap_value_min_max_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
@Preview
@Composable
fun NewPollVoteValueRangePreview() {
NewPollVoteValueRange(NewPostViewModel())
Column(
modifier = Modifier.fillMaxWidth()
) {
NewPollVoteValueRange(NewPostViewModel())
}
}

View File

@@ -133,13 +133,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 +163,7 @@ fun NewPostView(
) {
if (showRelaysDialog) {
RelaySelectionDialog(
list = relayList,
preSelectedList = relayList,
onClose = {
showRelaysDialog = false
},
@@ -518,6 +512,8 @@ private fun PollField(postViewModel: NewPostViewModel) {
NewPollOption(postViewModel, index)
}
NewPollVoteValueRange(postViewModel)
Button(
onClick = {
postViewModel.pollOptions[postViewModel.pollOptions.size] =
@@ -1374,8 +1370,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

@@ -73,8 +73,8 @@ open class NewPostViewModel() : ViewModel() {
var wantsPoll by mutableStateOf(false)
var zapRecipients = mutableStateListOf<HexKey>()
var pollOptions = newStateMapPollOptions()
var valueMaximum: Int? = null
var valueMinimum: Int? = null
var valueMaximum by mutableStateOf<Int?>(null)
var valueMinimum by mutableStateOf<Int?>(null)
var consensusThreshold: Int? = null
var closedAt: Int? = null
@@ -494,7 +494,7 @@ open class NewPostViewModel() : ViewModel() {
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
(!wantsZapraiser || zapRaiserAmount != null) &&
(!wantsDirectMessage || !toUsers.text.isNullOrBlank()) &&
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) &&
(!wantsPoll || (pollOptions.values.all { it.isNotEmpty() } && isValidvalueMinimum.value && isValidvalueMaximum.value)) &&
contentToAddUrl == null
}
@@ -612,6 +612,50 @@ open class NewPostViewModel() : ViewModel() {
nip24 = !nip24
}
}
fun updateMinZapAmountForPoll(textMin: String) {
if (textMin.isNotEmpty()) {
try {
val int = textMin.toInt()
if (int < 1) {
valueMinimum = null
} else {
valueMinimum = int
}
} catch (e: Exception) {}
} else {
valueMinimum = null
}
checkMinMax()
}
fun updateMaxZapAmountForPoll(textMax: String) {
if (textMax.isNotEmpty()) {
try {
val int = textMax.toInt()
if (int < 1) {
valueMaximum = null
} else {
valueMaximum = int
}
} catch (e: Exception) {}
} else {
valueMaximum = null
}
checkMinMax()
}
fun checkMinMax() {
if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) {
isValidvalueMinimum.value = false
isValidvalueMaximum.value = false
} else {
isValidvalueMinimum.value = true
isValidvalueMaximum.value = true
}
}
}
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

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

@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
@@ -12,6 +13,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDirection
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -41,10 +43,7 @@ fun MayBeWithdrawal(lnurlWord: String) {
@Composable
fun ClickableWithdrawal(withdrawalString: String) {
val context = LocalContext.current
val uri = remember(withdrawalString) {
Uri.parse("lightning:$withdrawalString")
}
val scope = rememberCoroutineScope()
val withdraw = remember(withdrawalString) {
AnnotatedString("$withdrawalString ")
@@ -53,9 +52,18 @@ fun ClickableWithdrawal(withdrawalString: String) {
ClickableText(
text = withdraw,
onClick = {
runCatching {
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
}
},
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)

View File

@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@@ -66,6 +67,7 @@ fun MayBeInvoicePreview(lnbcWord: String) {
@Composable
fun InvoicePreview(lnInvoice: String, amount: String?) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
@@ -118,9 +120,18 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
.fillMaxWidth()
.padding(vertical = 10.dp),
onClick = {
runCatching {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
}
},
shape = QuoteBorder,

View File

@@ -12,8 +12,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
@@ -37,10 +35,8 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel)
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
if (urlPreviewState == UrlPreviewState.Loading) {
LaunchedEffect(url) {
launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url) {
urlPreviewState = it
}
accountViewModel.urlPreview(url) {
urlPreviewState = it
}
}
}

View File

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

View File

@@ -7,7 +7,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@@ -50,7 +49,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
.reversed()
}
@OptIn(ExperimentalTime::class)
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
val (feed, elapsed) = measureTimedValue {
val me = account.userProfile()

View File

@@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.PrivateDmEvent
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@@ -38,7 +37,6 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
.reversed()
}
@OptIn(ExperimentalTime::class)
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
val (feed, elapsed) = measureTimedValue {
val me = account.userProfile()

View File

@@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal
import android.util.Log
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
abstract class FeedFilter<T> {
@OptIn(ExperimentalTime::class)
fun loadTop(): List<T> {
checkNotInMainThread()
@@ -33,7 +31,6 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
abstract fun applyFilter(collection: Set<T>): Set<T>
abstract fun sort(collection: Set<T>): List<T>
@OptIn(ExperimentalTime::class)
open fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
checkNotInMainThread()

View File

@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
@@ -34,6 +35,7 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent ||
it.event is PollNoteEvent ||
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedGeoHash(myTag) == true

View File

@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
@@ -34,6 +35,7 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent ||
it.event is PollNoteEvent ||
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedHash(myTag) == true

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

@@ -3,6 +3,9 @@ package com.vitorpamplona.amethyst.ui.navigation
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -80,7 +83,12 @@ fun AppNavigation(
}
}
NavHost(navController, startDestination = Route.Home.route) {
NavHost(
navController,
startDestination = Route.Home.route,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
Route.Home.let { route ->
composable(route.route, route.arguments, content = { it ->
val nip47 = it.arguments?.getString("nip47")
@@ -215,6 +223,7 @@ fun AppNavigation(
composable(route.route, route.arguments, content = {
ChatroomScreen(
roomId = it.arguments?.getString("id"),
draftMessage = it.arguments?.getString("message"),
accountViewModel = accountViewModel,
nav = nav
)

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
@@ -754,9 +752,9 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + " / " + LocalCache.notes.size)
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + " / " + LocalCache.addressables.size)
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + " / " + LocalCache.users.size)
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
@@ -39,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
@@ -56,12 +58,12 @@ import androidx.compose.ui.platform.LocalFocusManager
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.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
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
@@ -70,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
@@ -107,7 +110,7 @@ fun DrawerContent(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 100.dp),
.padding(top = 70.dp),
scaffoldState,
accountViewModel,
nav
@@ -160,7 +163,7 @@ fun ProfileContent(
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.height(120.dp)
)
} else {
Image(
@@ -169,7 +172,7 @@ fun ProfileContent(
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.height(120.dp)
)
}
@@ -218,7 +221,6 @@ fun ProfileContent(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 15.dp)
.clickable(
onClick = {
nav(route)
@@ -278,16 +280,21 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send,
capitalization = KeyboardCapitalization.Sentences
),
keyboardActions = KeyboardActions(
onSend = {
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
),
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
}
}
@@ -319,24 +326,26 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send,
capitalization = KeyboardCapitalization.Sentences
),
keyboardActions = KeyboardActions(
onSend = {
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
),
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
} else {
UserStatusDeleteButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.updateStatus(it, "")
accountViewModel.delete(it)
focusManager.clearFocus(true)
}
accountViewModel.deleteStatus(it)
focusManager.clearFocus(true)
}
}
}
@@ -446,7 +455,6 @@ fun ListContent(
}
val coroutineScope = rememberCoroutineScope()
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
var wantsToEditRelays by remember {
mutableStateOf(false)
}
@@ -482,7 +490,7 @@ fun ListContent(
)
IconRowRelays(
relayViewModel = relayViewModel,
accountViewModel = accountViewModel,
onClick = {
coroutineScope.launch {
scaffoldState.drawerState.close()
@@ -625,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
)
}
@@ -701,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()
@@ -728,7 +746,7 @@ fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
Spacer(modifier = Modifier.width(Size16dp))
RelayStatus(relayViewModel = relayViewModel)
RelayStatus(accountViewModel = accountViewModel)
}
}
}

View File

@@ -120,9 +120,12 @@ sealed class Route(
)
object Room : Route(
route = "Room/{id}",
route = "Room/{id}?message={message}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("message") { type = NavType.StringType; nullable = true; defaultValue = null }
).toImmutableList()
)
object RoomByAuthor : Route(

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,
@@ -625,7 +616,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
}
description?.let {
@@ -739,7 +730,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
}
description?.let {

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,
@@ -566,7 +561,7 @@ private fun StatusRow(
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
Spacer(modifier = DoubleHorzSpacer)
ReplyReaction(
baseNote = baseNote,

View File

@@ -42,7 +42,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
@@ -52,7 +51,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
@@ -70,8 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -352,8 +348,7 @@ private fun ParseAuthorCommentAndAmount(
}
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
launch(Dispatchers.IO) {
val newState = loadAmountState(zapRequest, zapEvent, accountViewModel)
accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState ->
if (newState != null) {
content.value = newState
}
@@ -363,34 +358,6 @@ private fun ParseAuthorCommentAndAmount(
onReady(content)
}
private suspend fun loadAmountState(
zapRequest: Note,
zapEvent: Note?,
accountViewModel: AccountViewModel
): ZapAmountCommentNotification? {
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = accountViewModel.decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
return ZapAmountCommentNotification(
newAuthor,
decryptedContent.content.ifBlank { null },
showAmountAxis(amount)
)
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
return ZapAmountCommentNotification(
zapRequest.author,
zapRequest.event?.content()?.ifBlank { null },
showAmountAxis(amount)
)
}
}
}
return null
}
@Composable
private fun RenderState(
content: MutableState<ZapAmountCommentNotification>,

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Icon
@@ -33,7 +34,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
@@ -45,7 +45,8 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -53,13 +54,11 @@ import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Composable
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String, accountViewModel: AccountViewModel): MutableState<Boolean?> {
val nip05Verified = remember(userMetadata.nip05) {
// starts with null if must verify or already filled in if verified in the last hour
val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
@@ -73,37 +72,9 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
if (nip05Verified.value == null) {
LaunchedEffect(key1 = userMetadata.nip05) {
launch(Dispatchers.IO) {
userMetadata.nip05?.ifBlank { null }?.let { nip05 ->
Nip05Verifier().verifyNip05(
nip05,
onSuccess = {
// Marks user as verified
if (it == pubkeyHex) {
userMetadata.nip05Verified = true
userMetadata.nip05LastVerificationTime = TimeUtils.now()
if (nip05Verified.value != true) {
nip05Verified.value = true
}
} else {
userMetadata.nip05Verified = false
userMetadata.nip05LastVerificationTime = 0
if (nip05Verified.value != false) {
nip05Verified.value = false
}
}
},
onError = {
userMetadata.nip05LastVerificationTime = 0
userMetadata.nip05Verified = false
if (nip05Verified.value != false) {
nip05Verified.value = false
}
}
)
accountViewModel.verifyNip05(userMetadata, pubkeyHex) { newVerificationStatus ->
if (nip05Verified.value != newVerificationStatus) {
nip05Verified.value = newVerificationStatus
}
}
}
@@ -154,7 +125,7 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (nip05 != null) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel)
if (nip05Verified.value != true) {
DisplayNIP05(nip05, nip05Verified)
@@ -180,7 +151,7 @@ fun RotateStatuses(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var indexToDisplay by remember {
var indexToDisplay by remember(statuses) {
mutableIntStateOf(0)
}
@@ -190,7 +161,7 @@ fun RotateStatuses(
LaunchedEffect(Unit) {
while (true) {
delay(10.seconds)
indexToDisplay = ((indexToDisplay + 1) % (statuses.size + 1))
indexToDisplay = (indexToDisplay + 1) % statuses.size
}
}
}
@@ -233,7 +204,7 @@ fun DisplayStatus(
"music" -> Icon(
painter = painterResource(id = R.drawable.tunestr),
null,
modifier = Size18Modifier,
modifier = Size15Modifier.padding(end = Size5dp),
tint = MaterialTheme.colors.placeholderText
)
else -> {}
@@ -249,6 +220,7 @@ fun DisplayStatus(
if (url != null) {
val uri = LocalUriHandler.current
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = { runCatching { uri.openUri(url.trim()) } }
@@ -263,6 +235,7 @@ fun DisplayStatus(
} else if (nostrATag != null) {
LoadAddressableNote(nostrATag) { note ->
if (note != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = {
@@ -284,6 +257,7 @@ fun DisplayStatus(
} else if (nostrHexID != null) {
LoadNote(baseNoteHex = nostrHexID) {
if (it != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = {
@@ -353,12 +327,12 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>, modifier:
}
@Composable
fun DisplayNip05ProfileStatus(user: User) {
fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
val uri = LocalUriHandler.current
user.nip05()?.let { nip05 ->
if (nip05.split("@").size <= 2) {
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex)
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
Row(verticalAlignment = Alignment.CenterVertically) {
NIP05VerifiedSymbol(nip05Verified, Size16Modifier)
var domainPadStart = 5.dp

View File

@@ -38,7 +38,6 @@ import androidx.compose.material.Text
import androidx.compose.material.darkColors
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 +82,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
@@ -165,6 +165,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
@@ -195,7 +196,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
@@ -276,11 +276,7 @@ fun CheckHiddenNoteCompose(
// Ignores reports as well
val state by remember {
mutableStateOf(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
AccountViewModel.NoteComposeReportState()
)
}
@@ -323,13 +319,6 @@ fun CheckHiddenNoteCompose(
}
}
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean,
val canPreview: Boolean,
val relevantReports: ImmutableSet<Note>
)
@Composable
fun LoadedNoteCompose(
note: Note,
@@ -346,19 +335,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
}
@@ -385,7 +369,7 @@ fun LoadedNoteCompose(
@Composable
fun RenderReportState(
state: NoteComposeReportState,
state: AccountViewModel.NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
@@ -404,6 +388,7 @@ fun RenderReportState(
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
isBoostedNote,
@@ -435,15 +420,13 @@ 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()
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState) {
launch(Dispatchers.Default) {
accountViewModel.isNoteAcceptable(note, onChange)
}
accountViewModel.isNoteAcceptable(note, onChange)
}
}
@@ -784,9 +767,9 @@ private fun ShortCommunityActionOptions(
nav: (String) -> Unit
) {
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
if (!isFollowing) {
@@ -1238,6 +1221,16 @@ fun routeFor(note: Note, loggedIn: User): String? {
return null
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
accountViewModel.account.userProfile().createChatroom(withKey)
return if (draftMessage != null) {
"Room/${withKey.hashCode()}?message=$draftMessage"
} else {
"Room/${withKey.hashCode()}"
}
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}
@@ -1315,7 +1308,7 @@ fun RenderPoll(
nav: (String) -> Unit
) {
val noteEvent = note.event as? PollNoteEvent ?: return
val eventContent = remember { noteEvent.content() }
val eventContent = remember(note) { noteEvent.content() }
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
@@ -1632,9 +1625,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()
)
}
@@ -1689,7 +1682,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,
@@ -1699,7 +1692,7 @@ fun DisplayRelaySet(
)
Column(modifier = Modifier.padding(start = 10.dp)) {
RelayOptionsAction(relay, accountViewModel, nav)
RelayOptionsAction(relay.url, accountViewModel, nav)
}
}
}
@@ -2399,46 +2392,47 @@ private fun ReplyRow(
) {
val noteEvent = note.event
val showReply by remember {
val showReply by remember(note) {
derivedStateOf {
noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
noteEvent is BaseTextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
}
}
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)
}
}
}
@@ -2671,14 +2665,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

@@ -98,9 +98,7 @@ private fun WatchZapsAndUpdateTallies(
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) {
pollViewModel.refreshTallies()
}
pollViewModel.refreshTallies()
}
}
@@ -132,8 +130,7 @@ private fun OptionNote(
ZapVote(
baseNote,
poolOption,
accountViewModel,
pollViewModel,
pollViewModel = pollViewModel,
nonClickablePrepend = {
RenderOptionAfterVote(
poolOption.descriptor,
@@ -147,18 +144,21 @@ private fun OptionNote(
)
},
clickablePrepend = {
}
},
accountViewModel = accountViewModel,
nav = nav
)
} else {
ZapVote(
baseNote,
poolOption,
accountViewModel,
pollViewModel,
pollViewModel = pollViewModel,
nonClickablePrepend = {},
clickablePrepend = {
RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav)
}
},
accountViewModel = accountViewModel,
nav = nav
)
}
}
@@ -269,11 +269,12 @@ private fun RenderOptionBeforeVote(
fun ZapVote(
baseNote: Note,
poolOption: PollOption,
accountViewModel: AccountViewModel,
pollViewModel: PollNoteViewModel,
modifier: Modifier = Modifier,
pollViewModel: PollNoteViewModel,
nonClickablePrepend: @Composable () -> Unit,
clickablePrepend: @Composable () -> Unit
clickablePrepend: @Composable () -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isLoggedUser by remember {
derivedStateOf {
@@ -283,6 +284,7 @@ fun ZapVote(
var wantsToZap by remember { mutableStateOf(false) }
var zappingProgress by remember { mutableStateOf(0f) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -384,7 +386,7 @@ fun ZapVote(
onError = {
scope.launch {
zappingProgress = 0f
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
showErrorMessageDialog = it
}
},
onProgress = {
@@ -395,6 +397,19 @@ fun ZapVote(
)
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
}
},
onDismiss = { showErrorMessageDialog = null }
)
}
clickablePrepend()
if (poolOption.zappedByLoggedIn) {

View File

@@ -3,13 +3,16 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
@@ -52,12 +55,12 @@ class PollNoteViewModel : ViewModel() {
closedAt = pollEvent?.getTagInt(CLOSED_AT)
}
suspend fun refreshTallies() {
totalZapped = totalZapped()
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
fun refreshTallies() {
viewModelScope.launch(Dispatchers.Default) {
totalZapped = totalZapped()
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
_tallies.emit(
pollOptions?.keys?.map {
val newOptions = pollOptions?.keys?.map {
val zappedInOption = zappedPollOptionAmount(it)
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
@@ -71,8 +74,12 @@ class PollNoteViewModel : ViewModel() {
val consensus = consensusThreshold != null && myTally >= consensusThreshold!!
PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
} ?: emptyList()
)
}
_tallies.emit(
newOptions ?: emptyList()
)
}
}
fun canZap(): Boolean {

View File

@@ -40,6 +40,7 @@ import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@@ -210,7 +211,7 @@ private fun InnerReactionRow(
) {
val (value, elapsed) = measureTimedValue {
Row(verticalAlignment = CenterVertically) {
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
}
}
Log.d("Rendering Metrics", "Reaction Zaps: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
@@ -241,7 +242,7 @@ private fun LoadAndDisplayZapraiser(
wantsToSeeReactions: MutableState<Boolean>,
accountViewModel: AccountViewModel
) {
val zapraiserAmount by remember {
val zapraiserAmount by remember(baseNote) {
derivedStateOf {
baseNote.event?.zapraiserAmount() ?: 0
}
@@ -261,42 +262,26 @@ private fun LoadAndDisplayZapraiser(
}
}
@Immutable
data class ZapraiserStatus(val progress: Float, val left: String)
@Composable
fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) {
val zapsState by baseNote.live().zaps.observeAsState()
var zapraiserProgress by remember { mutableStateOf(0F) }
var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") }
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) {
zapsState?.note?.let {
val newZapAmount = accountViewModel.calculateZapAmount(it)
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
if (percentage > 1) {
percentage = 1f
}
if (Math.abs(zapraiserProgress - percentage) > 0.001) {
val newZapraiserProgress = percentage
val newZapraiserLeft = if (percentage > 0.99) {
"0"
} else {
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
}
if (zapraiserLeft != newZapraiserLeft) {
zapraiserLeft = newZapraiserLeft
}
if (zapraiserProgress != newZapraiserProgress) {
zapraiserProgress = newZapraiserProgress
}
zapsState?.note?.let {
accountViewModel.calculateZapraiser(baseNote) { newStatus ->
if (zapraiserStatus != newStatus) {
zapraiserStatus = newStatus
}
}
}
}
val color = if (zapraiserProgress > 0.99) {
val color = if (zapraiserStatus.progress > 0.99) {
DarkerGreen
} else {
MaterialTheme.colors.mediumImportanceLink
@@ -307,7 +292,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
.fillMaxWidth()
.height(if (details) 24.dp else 4.dp),
color = color,
progress = zapraiserProgress
progress = zapraiserStatus.progress
)
if (details) {
@@ -315,14 +300,14 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
contentAlignment = Center,
modifier = TinyBorders
) {
val totalPercentage by remember(zapraiserProgress) {
val totalPercentage by remember(zapraiserStatus) {
derivedStateOf {
"${(zapraiserProgress * 100).roundToInt()}%"
"${(zapraiserStatus.progress * 100).roundToInt()}%"
}
}
Text(
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft),
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
modifier = NoSoTinyBorders,
color = MaterialTheme.colors.placeholderText,
fontSize = Font14SP,
@@ -334,15 +319,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
@Composable
private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: @Composable () -> Unit) {
val hasReactions by baseNote.live().zaps.combineWith(
liveData1 = baseNote.live().boosts,
liveData2 = baseNote.live().reactions,
block = { zapsState, boostsState, reactionsState ->
zapsState?.note?.zaps?.isNotEmpty() == true ||
boostsState?.note?.boosts?.isNotEmpty() == true ||
reactionsState?.note?.reactions?.isNotEmpty() == true
}
).observeAsState(
val hasReactions by baseNote.live().hasReactions.observeAsState(
baseNote.zaps.isNotEmpty() ||
baseNote.boosts.isNotEmpty() ||
baseNote.reactions.isNotEmpty()
@@ -435,17 +412,16 @@ private fun WatchBoostsAndRenderGallery(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostsEvents by remember(boostsState) {
derivedStateOf { baseNote.boosts.toImmutableList() }
}
val boostsEvents by baseNote.live().boostList.observeAsState()
if (boostsEvents.isNotEmpty()) {
RenderBoostGallery(
boostsEvents,
nav,
accountViewModel
)
boostsEvents?.let {
if (it.isNotEmpty()) {
RenderBoostGallery(
it,
nav,
accountViewModel
)
}
}
}
@@ -836,10 +812,7 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
val reactionsState by baseNote.live().reactions.observeAsState()
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile())
onNewReactionType(reactionNote)
}
accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType)
}
}
@@ -874,32 +847,9 @@ private fun RenderReactionType(
@Composable
fun LikeText(baseNote: Note, grayTint: Color) {
val reactionsCount = remember(baseNote) {
mutableStateOf(baseNote.reactions.size)
}
val reactionCount by baseNote.live().reactionCount.observeAsState(0)
val scope = rememberCoroutineScope()
WatchReactionCountForNote(baseNote) { newReactionsCount ->
if (reactionsCount.value != newReactionsCount) {
scope.launch(Dispatchers.Main) {
reactionsCount.value = newReactionsCount
}
}
}
SlidingAnimationCount(reactionsCount, grayTint)
}
@Composable
private fun WatchReactionCountForNote(baseNote: Note, onNewReactionCount: (Int) -> Unit) {
val reactionsState by baseNote.live().reactions.observeAsState()
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
onNewReactionCount(reactionsState?.note?.countReactions() ?: 0)
}
}
SlidingAnimationCount(reactionCount, grayTint)
}
private fun likeClick(
@@ -950,11 +900,13 @@ fun ZapReaction(
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = 20.dp,
animationSize: Dp = 14.dp
animationSize: Dp = 14.dp,
nav: (String) -> Unit
) {
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -980,6 +932,12 @@ fun ZapReaction(
},
onMultipleChoices = {
wantsToZap = true
},
onError = {
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
}
)
},
@@ -1006,7 +964,7 @@ fun ZapReaction(
onError = {
scope.launch {
zappingProgress = 0f
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
showErrorMessageDialog = it
}
},
onProgress = {
@@ -1017,12 +975,43 @@ fun ZapReaction(
)
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
}
},
onDismiss = { showErrorMessageDialog = null }
)
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
UpdateZapAmountDialog(
onClose = { wantsToChangeZapAmount = false },
accountViewModel = accountViewModel
)
}
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
}
},
accountViewModel = accountViewModel,
baseNote = baseNote
)
}
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
@@ -1055,7 +1044,8 @@ private fun zapClick(
scope: CoroutineScope,
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit
onMultipleChoices: () -> Unit,
onError: (String) -> Unit
) {
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
scope.launch {
@@ -1084,14 +1074,7 @@ 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)
@@ -1139,9 +1122,7 @@ private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel,
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) {
onWasZapped(accountViewModel.calculateIfNoteWasZappedByAccount(baseNote))
}
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped)
}
}
@@ -1171,9 +1152,7 @@ fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, o
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
launch(Dispatchers.Default) {
onZapAmount(showAmount(accountViewModel.calculateZapAmount(baseNote)))
}
accountViewModel.calculateZapAmount(baseNote, onZapAmount)
}
}

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

@@ -73,9 +73,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) {
@@ -112,23 +110,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 +268,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,
@@ -508,9 +500,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,14 +1,16 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.runtime.*
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.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
@@ -28,8 +30,6 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ZapOptionstViewModel : ViewModel() {
private var account: Account? = null
@@ -58,17 +58,20 @@ class ZapOptionstViewModel : ViewModel() {
}
@Composable
fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, baseNote: Note) {
fun ZapCustomDialog(
onClose: () -> Unit,
onError: (text: String) -> Unit,
onProgress: (percent: Float) -> 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)),
@@ -108,18 +111,8 @@ 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,
zapType = selectedZapType
)
onClose()
@@ -225,3 +218,51 @@ fun ZapButton(isActive: Boolean, onPost: () -> Unit) {
Text(text = "⚡Zap ", color = Color.White)
}
}
@Composable
fun ErrorMessageDialog(
title: String,
textContent: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickStartMessage: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(title)
},
text = {
Text(textContent)
},
buttons = {
Row(
modifier = Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(onClick = onClickStartMessage) {
Icon(
painter = painterResource(R.drawable.ic_dm),
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.error_dialog_talk_to_user))
}
Button(onClick = onDismiss, colors = buttonColors) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.error_dialog_button_ok))
}
}
}
}
)
}

View File

@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -102,11 +101,9 @@ private fun WatchScrollToTop(
val scrollToTop by viewModel.scrollToTop.collectAsState()
LaunchedEffect(scrollToTop) {
launch {
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
listState.scrollToItem(index = 0)
viewModel.sentToTop()
}
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
listState.scrollToItem(index = 0)
viewModel.sentToTop()
}
}
}

View File

@@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@Stable
@@ -270,7 +269,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
private val bundler = BundledUpdate(1000, Dispatchers.IO)
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
@OptIn(ExperimentalTime::class)
fun invalidateData(ignoreIfDoing: Boolean = false) {
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
@@ -282,7 +280,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
}
@OptIn(ExperimentalTime::class)
fun invalidateDataAndSendToTop() {
clear()
bundler.invalidate(false) {
@@ -296,7 +293,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
}
@OptIn(ExperimentalTime::class)
fun checkKeysInvalidateDataAndSendToTop() {
if (lastFeedKey != localFilter.feedKey()) {
clear()
@@ -312,7 +308,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
}
@OptIn(ExperimentalTime::class)
fun invalidateInsertData(newItems: Set<Note>) {
bundlerInsert.invalidateList(newItems) {
val newObjects = it.flatten().toSet()

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
}
}
}
@@ -236,7 +236,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
checkNotInMainThread()
lastFeedKey = localFilter.feedKey()
val notes = localFilter.loadTop().toImmutableList()
val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList()
val oldNotesState = _feedContent.value
if (oldNotesState is FeedState.Loaded) {
@@ -301,11 +301,13 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
fun checkKeysInvalidateDataAndSendToTop() {
if (lastFeedKey != localFilter.feedKey()) {
bundler.invalidate(false) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
refreshSuspended()
sendToTop()
viewModelScope.launch(Dispatchers.IO) {
bundler.invalidate(false) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
refreshSuspended()
sendToTop()
}
}
}
}

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

@@ -3,6 +3,7 @@ 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
@@ -16,17 +17,31 @@ 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.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.quartz.encoders.HexKey
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.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
@@ -45,22 +60,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
) {
@@ -121,21 +140,86 @@ class AccountViewModel(val account: Account) : ViewModel() {
account.delete(account.boostsTo(note))
}
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
return account.calculateIfNoteWasZappedByAccount(zappedNote)
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.Default) {
onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote))
}
}
fun calculateZapAmount(zappedNote: Note): BigDecimal {
suspend fun calculateZapAmount(zappedNote: Note): BigDecimal {
return account.calculateZappedAmount(zappedNote)
}
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
}
}
fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
val newZapAmount = calculateZapAmount(zappedNote)
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
if (percentage > 1) {
percentage = 1f
}
val newZapraiserProgress = percentage
val newZapraiserLeft = if (percentage > 0.99) {
"0"
} else {
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
}
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
}
}
fun decryptAmountMessage(
zapRequest: Note,
zapEvent: Note?,
onNewState: (ZapAmountCommentNotification?) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
onNewState(innerDecryptAmountMessage(zapRequest, zapEvent))
}
}
private suspend fun innerDecryptAmountMessage(
zapRequest: Note,
zapEvent: Note?
): ZapAmountCommentNotification? {
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
return ZapAmountCommentNotification(
newAuthor,
decryptedContent.content.ifBlank { null },
showAmountAxis(amount)
)
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
return ZapAmountCommentNotification(
zapRequest.author,
zapRequest.event?.content()?.ifBlank { null },
showAmountAxis(amount)
)
}
}
}
return null
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
viewModelScope.launch(Dispatchers.IO) {
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
}
}
suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.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()) {
@@ -186,9 +270,12 @@ class AccountViewModel(val account: Account) : ViewModel() {
onProgress(0f)
}
} else {
runCatching {
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)
}
@@ -331,25 +418,44 @@ class AccountViewModel(val account: Account) : ViewModel() {
return account.defaultZapType
}
fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet<Note>) -> Unit) {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean = true,
val canPreview: Boolean = true,
val isHiddenAuthor: Boolean = false,
val relevantReports: ImmutableSet<Note> = persistentSetOf()
)
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onReady(true, true, persistentSetOf())
} else {
val newCanPreview = !note.hasAnyReports()
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
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onReady(true, true, persistentSetOf())
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else if (note.author?.let { account.isHidden(it) } == true) {
onReady(NoteComposeReportState(false, false, true, persistentSetOf()))
} else {
val newRelevantReports = account.getRelevantReports(note)
val newCanPreview = !note.hasAnyReports()
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else {
val newRelevantReports = account.getRelevantReports(note)
onReady(
NoteComposeReportState(
newIsAcceptable,
newCanPreview,
false,
newRelevantReports.toImmutableSet()
)
)
}
}
}
}
@@ -380,11 +486,82 @@ class AccountViewModel(val account: Account) : ViewModel() {
}
fun createStatus(newStatus: String) {
account.createStatus(newStatus)
viewModelScope.launch(Dispatchers.IO) {
account.createStatus(newStatus)
}
}
fun updateStatus(it: AddressableNote, newStatus: String) {
account.updateStatus(it, newStatus)
viewModelScope.launch(Dispatchers.IO) {
account.updateStatus(it, newStatus)
}
}
fun deleteStatus(it: AddressableNote) {
viewModelScope.launch(Dispatchers.IO) {
account.deleteStatus(it)
}
}
fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val isOnline = OnlineChecker.isOnline(url)
onResult(isOnline)
}
}
fun urlPreview(url: String, onResult: suspend (UrlPreviewState) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url, onResult)
}
}
fun loadReactionTo(note: Note?, onNewReactionType: (String?) -> Unit) {
if (note == null) return
viewModelScope.launch(Dispatchers.Default) {
onNewReactionType(note.getReactionBy(userProfile()))
}
}
fun verifyNip05(userMetadata: UserMetadata, pubkeyHex: String, onResult: (Boolean) -> Unit) {
val nip05 = userMetadata.nip05?.ifBlank { null } ?: return
viewModelScope.launch(Dispatchers.IO) {
Nip05Verifier().verifyNip05(
nip05,
onSuccess = {
// Marks user as verified
if (it == pubkeyHex) {
userMetadata.nip05Verified = true
userMetadata.nip05LastVerificationTime = TimeUtils.now()
onResult(userMetadata.nip05Verified)
} else {
userMetadata.nip05Verified = false
userMetadata.nip05LastVerificationTime = 0
onResult(userMetadata.nip05Verified)
}
},
onError = {
userMetadata.nip05LastVerificationTime = 0
userMetadata.nip05Verified = false
onResult(userMetadata.nip05Verified)
}
)
}
}
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 {

View File

@@ -623,7 +623,7 @@ fun ShowVideoStreaming(
}
url?.let {
CheckIfUrlIsOnline(url) {
CheckIfUrlIsOnline(url, accountViewModel) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = remember { Modifier.heightIn(max = 300.dp) }
@@ -893,7 +893,7 @@ private fun ShortChannelActionOptions(
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
Spacer(modifier = StdHorzSpacer)
}
}
@@ -968,7 +968,7 @@ private fun LiveChannelActionOptions(
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
}
}

View File

@@ -101,6 +101,7 @@ import kotlinx.coroutines.withContext
@Composable
fun ChatroomScreen(
roomId: String?,
draftMessage: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -110,6 +111,7 @@ fun ChatroomScreen(
it?.let {
PrepareChatroomViewModels(
room = it,
draftMessage = draftMessage,
accountViewModel = accountViewModel,
nav = nav
)
@@ -120,6 +122,7 @@ fun ChatroomScreen(
@Composable
fun ChatroomScreenByAuthor(
authorPubKeyHex: String?,
draftMessage: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -129,6 +132,7 @@ fun ChatroomScreenByAuthor(
it?.let {
PrepareChatroomViewModels(
room = it,
draftMessage = draftMessage,
accountViewModel = accountViewModel,
nav = nav
)
@@ -166,7 +170,12 @@ fun LoadRoomByAuthor(authorPubKeyHex: String, accountViewModel: AccountViewModel
}
@Composable
fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun PrepareChatroomViewModels(
room: ChatroomKey,
draftMessage: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val feedViewModel: NostrChatroomFeedViewModel = viewModel(
key = room.hashCode().toString() + "ChatroomViewModels",
factory = NostrChatroomFeedViewModel.Factory(
@@ -193,6 +202,12 @@ fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewMo
}
}
if (draftMessage != null) {
LaunchedEffect(key1 = draftMessage) {
newPostModel.message = TextFieldValue(draftMessage)
}
}
ChatroomScreen(
room = room,
feedViewModel = feedViewModel,

View File

@@ -29,7 +29,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
@@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -136,12 +134,14 @@ private fun HomePages(
}
@Composable
fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) {
fun CheckIfUrlIsOnline(url: String, accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit) {
var online by remember { mutableStateOf(false) }
LaunchedEffect(key1 = url) {
launch(Dispatchers.IO) {
online = OnlineChecker.isOnline(url)
accountViewModel.checkIfOnline(url) { isOnline ->
if (online != isOnline) {
online = isOnline
}
}
}
@@ -162,11 +162,9 @@ fun WatchAccountForHomeScreen(
val followState by accountViewModel.account.userProfile().live().follows.observeAsState()
LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) {
launch(Dispatchers.IO) {
NostrHomeDataSource.invalidateFilters()
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
}
NostrHomeDataSource.invalidateFilters()
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
}
}

View File

@@ -73,42 +73,38 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = noteState) {
scope.launch {
val note = noteState?.note ?: return@launch
var event = note.event
val channelHex = note.channelHex()
val note = noteState?.note ?: return@LaunchedEffect
var event = note.event
val channelHex = note.channelHex()
if (event is GiftWrapEvent) {
event = accountViewModel.unwrap(event)
}
if (event is GiftWrapEvent) {
event = accountViewModel.unwrap(event)
}
if (event is SealedGossipEvent) {
event = accountViewModel.unseal(event)
}
if (event is SealedGossipEvent) {
event = accountViewModel.unseal(event)
}
if (event == null) {
// stay here, loading
} else if (event is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
} else if (event is ChatroomKeyable) {
note.author?.let {
val withKey = (event as ChatroomKeyable)
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
if (event == null) {
// stay here, loading
} else if (event is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
} else if (event is ChatroomKeyable) {
note.author?.let {
val withKey = (event as ChatroomKeyable)
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
withContext(Dispatchers.IO) {
accountViewModel.userProfile().createChatroom(withKey)
}
nav("Room/${withKey.hashCode()}")
withContext(Dispatchers.IO) {
accountViewModel.userProfile().createChatroom(withKey)
}
} else if (channelHex != null) {
nav("Channel/$channelHex")
} else {
nav("Note/${note.idHex}")
nav("Room/${withKey.hashCode()}")
}
} else if (channelHex != null) {
nav("Channel/$channelHex")
} else {
nav("Note/${note.idHex}")
}
}

View File

@@ -920,7 +920,7 @@ private fun DrawAdditionalInfo(
DisplayBadges(baseUser, nav)
DisplayNip05ProfileStatus(user)
DisplayNip05ProfileStatus(user, accountViewModel)
val website = user.info?.website
if (!website.isNullOrEmpty()) {
@@ -1050,9 +1050,18 @@ fun DisplayLNAddress(
}
}
} else {
runCatching {
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) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
}
}
},

View File

@@ -23,12 +23,13 @@ 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)
LaunchedEffect(noteId) {
NostrThreadDataSource.loadThread(noteId)
feedViewModel.invalidateData()
feedViewModel.invalidateData(true)
}
DisposableEffect(accountViewModel) {
@@ -37,7 +38,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
println("Thread Start")
NostrThreadDataSource.loadThread(noteId)
NostrThreadDataSource.start()
feedViewModel.invalidateData()
feedViewModel.invalidateData(true)
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Thread Stop")

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
@@ -250,19 +249,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 +275,7 @@ fun LoadedVideoCompose(
@Composable
fun RenderReportState(
state: NoteComposeReportState,
state: AccountViewModel.NoteComposeReportState,
note: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -290,11 +284,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 +416,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)
}
}
}
@@ -467,7 +459,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
wantsToQuote = baseNote
}
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp)
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav)
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
}
}

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,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">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,46 @@
<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>
</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,46 @@ 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>
</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,477 @@
<?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>
</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,8 +101,8 @@
<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="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage</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>
<string name="join">Joindre</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,63 +400,78 @@
<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>
<string name="application_preferences">Préférences de l\'application</string>
<string name="language">Langage</string>
<string name="theme">Thème</string>
<string name="automatically_load_images_gifs">Chargement automatique des images/gifs</string>
<string name="automatically_play_videos">Démarrage automatique des vidéos</string>
<string name="automatically_show_url_preview">Prévisualisation automatique des liens</string>
<string name="automatically_load_images_gifs">Prévisualisation des images</string>
<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_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>
</resources>

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