mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227de74317 | ||
|
|
0b9fced8bc | ||
|
|
274e4f7498 | ||
|
|
f6496b5e7e | ||
|
|
ee5ff1845f | ||
|
|
c84e835ce5 | ||
|
|
cdb5be2fe9 | ||
|
|
2d0c356aa5 | ||
|
|
0a92c253b8 | ||
|
|
92347863bc | ||
|
|
a146ec5e1a | ||
|
|
83be43e94e | ||
|
|
d726b3b5b5 | ||
|
|
c845aac8e5 | ||
|
|
1cdf114fff | ||
|
|
4d1a99d076 | ||
|
|
b1debd9879 | ||
|
|
3843917bd1 | ||
|
|
2fa4281b6f | ||
|
|
c42944e15b | ||
|
|
9f9f158709 | ||
|
|
3a753f0d5d | ||
|
|
13c94112d4 | ||
|
|
dd46f12a05 | ||
|
|
9a648d7e84 | ||
|
|
614dbab1fd | ||
|
|
3d80ee1984 | ||
|
|
536fb49bc3 | ||
|
|
edd93141bc | ||
|
|
20c2d19a9c | ||
|
|
17b2fe9cd8 | ||
|
|
134e7cee0c | ||
|
|
108ddffcc7 | ||
|
|
20e76daff8 | ||
|
|
6876a7f0d2 | ||
|
|
87dbf86129 | ||
|
|
175d1ae33b | ||
|
|
bf9d318209 | ||
|
|
7bee44c5ad | ||
|
|
a32e90aed0 | ||
|
|
310db1008e | ||
|
|
21560d098e | ||
|
|
33f7c5c82e | ||
|
|
52f17e9f46 | ||
|
|
1e7ffa365b | ||
|
|
e00fc83e5c | ||
|
|
a2fcb3539e | ||
|
|
cb69d056b8 | ||
|
|
e7f950cd32 | ||
|
|
3b2686ffeb | ||
|
|
2387a609df | ||
|
|
6457a84f4b | ||
|
|
dee231d43a | ||
|
|
8c4af4ac85 | ||
|
|
58cf0d9f5d | ||
|
|
de5c0d4681 | ||
|
|
780832d785 | ||
|
|
b8808a17b5 | ||
|
|
5001e58c2b | ||
|
|
95514b66ea | ||
|
|
90c6d6c8f8 | ||
|
|
88d2ce3ad0 | ||
|
|
484f3a6842 | ||
|
|
866caf8bc5 | ||
|
|
be9e3c3bf7 | ||
|
|
49a3cb1978 | ||
|
|
c3ddc4f7ad | ||
|
|
ff4d704bde | ||
|
|
a773be12c4 | ||
|
|
7ff6f16df0 | ||
|
|
d9053f580f | ||
|
|
fee2764338 | ||
|
|
61cb4fdd74 | ||
|
|
d89869ebff | ||
|
|
8518819dfa | ||
|
|
0a8923be6b | ||
|
|
cda821e5af | ||
|
|
be2402702f | ||
|
|
d723a8ab5c | ||
|
|
5c882c81c8 | ||
|
|
24d2709564 | ||
|
|
ce15bc46a7 | ||
|
|
de4bc30730 | ||
|
|
64f6811c00 | ||
|
|
e05ab104b9 | ||
|
|
b4f1c4d13e | ||
|
|
36a3c4123c | ||
|
|
939eb1bd8d | ||
|
|
854469cc7d | ||
|
|
338ae41baa | ||
|
|
6cae89d276 | ||
|
|
15c1e25122 | ||
|
|
c5716416b6 | ||
|
|
65e4a766ab | ||
|
|
c2a4c9bbb3 | ||
|
|
b042110154 | ||
|
|
da97b5593b | ||
|
|
44b4b0cdb9 | ||
|
|
190b7fe819 | ||
|
|
8eb89f0fe9 | ||
|
|
3085353729 | ||
|
|
1465ce112c | ||
|
|
5c797858ba | ||
|
|
8c51c4be0c | ||
|
|
4e8ecff67b | ||
|
|
6303573dd9 | ||
|
|
83f8c51aca | ||
|
|
e675348461 | ||
|
|
9026f5bf07 | ||
|
|
a7aa3705ef | ||
|
|
a64c941f36 | ||
|
|
02f18b5b35 | ||
|
|
db740f4747 | ||
|
|
6a3206b938 | ||
|
|
23414b0bc6 | ||
|
|
0234cfca30 | ||
|
|
f707714611 | ||
|
|
7b90230c45 | ||
|
|
d3468f3eba | ||
|
|
fc9500b45c | ||
|
|
df0b5fcd19 | ||
|
|
59cde5699e | ||
|
|
bd836a9580 | ||
|
|
90aade338c | ||
|
|
40e2dba490 | ||
|
|
d13e8bacec | ||
|
|
d3fa05a4df | ||
|
|
734dd2e119 | ||
|
|
a9ea9ea2ae | ||
|
|
1b521bc140 | ||
|
|
364e3c5f5f | ||
|
|
647cda3f90 | ||
|
|
bcca4d4bcc | ||
|
|
0b038dca17 | ||
|
|
ad6f63df56 | ||
|
|
b5b8b62a16 | ||
|
|
8d3066b377 | ||
|
|
18933b4326 | ||
|
|
672841c0ed | ||
|
|
872c7cb257 | ||
|
|
6d691c4741 | ||
|
|
bbba27752d | ||
|
|
d9cf090cc9 | ||
|
|
46effa572c | ||
|
|
f76ab01c11 | ||
|
|
bfae5871c8 | ||
|
|
451c5d1648 | ||
|
|
3423626c60 | ||
|
|
8982f02a4a | ||
|
|
f06b5fdacf | ||
|
|
8ea8cf2985 | ||
|
|
2771d34951 | ||
|
|
669b33a3e1 | ||
|
|
c934a1cdfe | ||
|
|
0ccd41f5e8 | ||
|
|
1af3bb7dfc | ||
|
|
f0fcf1ea8a | ||
|
|
5325542c81 | ||
|
|
3d9675841b | ||
|
|
0d475fba0e | ||
|
|
05714de945 | ||
|
|
89c1b53c95 | ||
|
|
d1cb699027 | ||
|
|
9ee1250f3a | ||
|
|
7a68cf867e | ||
|
|
1c9041f12b | ||
|
|
d3de5e0a42 | ||
|
|
b1bd21533d | ||
|
|
51f997a34a | ||
|
|
d5607faadd | ||
|
|
1a9c3ccb79 | ||
|
|
bc8fa00608 | ||
|
|
329d806ffd | ||
|
|
e040d0f971 | ||
|
|
053de4d06f | ||
|
|
4016458540 | ||
|
|
8b98efaad7 | ||
|
|
217ed90d9a | ||
|
|
b82aefbd5d | ||
|
|
63b954d38d | ||
|
|
76cdea0087 | ||
|
|
da1a3a8f85 | ||
|
|
d11e92e166 | ||
|
|
61efd237c2 | ||
|
|
6099948605 | ||
|
|
45b743e7c8 | ||
|
|
108c6cbeb4 | ||
|
|
da02891c6a | ||
|
|
a03e9ea946 | ||
|
|
dd4a6f8da0 | ||
|
|
c01033cbb0 | ||
|
|
1fe93395c5 | ||
|
|
cf960fe59d | ||
|
|
2416d41131 | ||
|
|
0699095d77 | ||
|
|
6ed3e84d95 | ||
|
|
6c196cfeb7 | ||
|
|
987784fb89 | ||
|
|
d60ac5d73b | ||
|
|
c2361c034c | ||
|
|
199514e7f3 | ||
|
|
a1364d19c2 | ||
|
|
e73428b156 | ||
|
|
a684a222cb | ||
|
|
1646cbbd68 | ||
|
|
035a48a0cd | ||
|
|
71be8ce239 | ||
|
|
1ccf9bf467 | ||
|
|
42a0cd0b1b | ||
|
|
39547b1d11 | ||
|
|
c223c1372b | ||
|
|
a450dcbac1 | ||
|
|
ee5fbce2ce | ||
|
|
559b88be18 | ||
|
|
dfbc5fa556 | ||
|
|
df1a017eb3 | ||
|
|
b2b7799a0f | ||
|
|
e817f94045 | ||
|
|
46571a6029 | ||
|
|
4f4d689f5a | ||
|
|
a0308938de | ||
|
|
b10e875941 | ||
|
|
632400f52c | ||
|
|
b63479c45f | ||
|
|
7ea6495f28 | ||
|
|
453e63025d | ||
|
|
17ecc98e1e | ||
|
|
f9272f5cfc | ||
|
|
348e07a307 | ||
|
|
2009f5bd7b | ||
|
|
7ea92bc068 | ||
|
|
3b07b5d8dd | ||
|
|
f06e79cfd3 | ||
|
|
584ceb1f0f | ||
|
|
8053119d42 | ||
|
|
b8e97525eb | ||
|
|
66b1e4b15d | ||
|
|
e6f6f64512 | ||
|
|
d2eea9f0ae | ||
|
|
012fdc8cc6 | ||
|
|
74054aab5a | ||
|
|
11908b9e0c | ||
|
|
570cccfc62 | ||
|
|
de9d6877e1 | ||
|
|
c31fa116ca | ||
|
|
10b15226a1 | ||
|
|
7a46376bc1 | ||
|
|
b761aaf970 | ||
|
|
3a08db2dfb | ||
|
|
41fd2e087c | ||
|
|
ba476bcdca |
44
.github/workflows/create-release.yml
vendored
44
.github/workflows/create-release.yml
vendored
@@ -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
30
.github/workflows/crowdin.yml
vendored
Normal 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 }}
|
||||
32
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
32
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
10
README.md
10
README.md
@@ -106,7 +106,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
|
||||
This is a native Android app made with Kotlin and Jetpack Compose.
|
||||
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
|
||||
The overall architecture consists in the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
|
||||
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
|
||||
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
|
||||
|
||||
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
|
||||
@@ -172,7 +172,7 @@ For the Play build:
|
||||
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
|
||||
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
|
||||
```
|
||||
2. Create 4 Secret Key variables on your GitHub repository and fill in with the signing key information
|
||||
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
|
||||
- `KEY_ALIAS` <- `<alias_name>`
|
||||
- `KEY_PASSWORD` <- `<your password>`
|
||||
- `KEY_STORE_PASSWORD` <- `<your key store password>`
|
||||
@@ -192,15 +192,15 @@ The relay also learns which public keys you are requesting, meaning your public
|
||||
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
|
||||
|
||||
# DM Privacy #
|
||||
While the content of direct messages (DMs) is only visible to you, and your DM nostr counterparty, everyone can see that and when you and your counterparty are DM-ing each other.
|
||||
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
|
||||
|
||||
# Visibility & Permanence of Your Content on nostr
|
||||
## Information Visibility ##
|
||||
Content that you share can be shared to other relays.
|
||||
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
|
||||
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
|
||||
|
||||
## Information Permanence ##
|
||||
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
|
||||
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
|
||||
|
||||
# Screenshots
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 287
|
||||
versionName "0.75.8"
|
||||
versionCode 297
|
||||
versionName "0.77.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
|
||||
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'in', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -54,7 +54,7 @@ android {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include "x86", "x86_64"
|
||||
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// Zoomable images
|
||||
implementation "net.engawapg.lib:zoomable:1.5.0"
|
||||
implementation 'net.engawapg.lib:zoomable:1.5.1'
|
||||
|
||||
// Biometrics
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
|
||||
class PushNotificationUtils {
|
||||
fun init(accounts: List<AccountInfo>) {
|
||||
object PushNotificationUtils {
|
||||
var hasInit: Boolean = true
|
||||
suspend fun init(accounts: List<AccountInfo>) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner.debug"/>
|
||||
</queries>
|
||||
|
||||
<!-- Doesn't require a camera -->
|
||||
|
||||
@@ -35,7 +35,8 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
|
||||
@Immutable
|
||||
data class AccountInfo(
|
||||
val npub: String,
|
||||
val hasPrivKey: Boolean = false
|
||||
val hasPrivKey: Boolean,
|
||||
val loggedInWithExternalSigner: Boolean
|
||||
)
|
||||
|
||||
private object PrefKeys {
|
||||
@@ -74,6 +75,7 @@ private object PrefKeys {
|
||||
const val THEME = "theme"
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ object LocalPreferences {
|
||||
|
||||
private var _currentAccount: String? = null
|
||||
|
||||
private fun currentAccount(): String? {
|
||||
fun currentAccount(): String? {
|
||||
if (_currentAccount == null) {
|
||||
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
}
|
||||
@@ -201,7 +203,11 @@ object LocalPreferences {
|
||||
|
||||
fun allSavedAccounts(): List<AccountInfo> {
|
||||
return savedAccounts().map { npub ->
|
||||
AccountInfo(npub = npub)
|
||||
AccountInfo(
|
||||
npub,
|
||||
hasPrivKey(npub),
|
||||
getLoggedInWithExternalSigner(npub)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +251,7 @@ object LocalPreferences {
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
}
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
|
||||
}.apply()
|
||||
|
||||
val globalPrefs = encryptedPreferences()
|
||||
@@ -276,17 +283,19 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
fun getTheme(): Int {
|
||||
encryptedPreferences().apply {
|
||||
return getInt(PrefKeys.THEME, 0)
|
||||
}
|
||||
return encryptedPreferences().getInt(PrefKeys.THEME, 0)
|
||||
}
|
||||
|
||||
fun getPreferredLanguage(): String {
|
||||
var language = ""
|
||||
encryptedPreferences().apply {
|
||||
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
|
||||
}
|
||||
return language
|
||||
return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
|
||||
}
|
||||
|
||||
private fun getLoggedInWithExternalSigner(npub: String): Boolean {
|
||||
return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
}
|
||||
|
||||
private fun hasPrivKey(npub: String): Boolean {
|
||||
return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
|
||||
}
|
||||
|
||||
fun loadFromEncryptedStorage(): Account? {
|
||||
@@ -363,6 +372,7 @@ object LocalPreferences {
|
||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
|
||||
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||
@@ -431,7 +441,8 @@ object LocalPreferences {
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
settings = settings
|
||||
settings = settings,
|
||||
loginWithExternalSigner = loginWithExternalSigner
|
||||
)
|
||||
|
||||
return a
|
||||
|
||||
@@ -92,9 +92,7 @@ class MultiPlayerPlaybackManager(
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
when (playbackState) {
|
||||
STATE_IDLE -> {
|
||||
if (player.currentPosition > 5 * 60) { // 5 seconds
|
||||
cachedPositions.add(uri, player.currentPosition)
|
||||
}
|
||||
cachedPositions.add(uri, player.currentPosition)
|
||||
}
|
||||
STATE_READY -> {
|
||||
cachedPositions.get(uri)?.let { lastPosition ->
|
||||
@@ -104,9 +102,7 @@ class MultiPlayerPlaybackManager(
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (player.currentPosition > 5 * 60) { // 5 seconds
|
||||
cachedPositions.add(uri, player.currentPosition)
|
||||
}
|
||||
cachedPositions.add(uri, player.currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
@@ -35,15 +36,21 @@ import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import java.io.File
|
||||
|
||||
object ServiceManager {
|
||||
var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var account: Account? = null
|
||||
|
||||
fun start(account: Account, context: Context) {
|
||||
this.account = account
|
||||
ExternalSignerUtils.account = account
|
||||
start(context)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun start(context: Context) {
|
||||
if (isStarted && account != null) {
|
||||
return
|
||||
}
|
||||
Log.d("ServiceManager", "Starting Relay Services")
|
||||
|
||||
val myAccount = account
|
||||
@@ -88,6 +95,7 @@ object ServiceManager {
|
||||
NostrSingleEventDataSource.start()
|
||||
NostrSingleChannelDataSource.start()
|
||||
NostrSingleUserDataSource.start()
|
||||
isStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +121,7 @@ object ServiceManager {
|
||||
NostrVideoDataSource.stop()
|
||||
|
||||
Client.disconnect()
|
||||
isStarted = false
|
||||
}
|
||||
|
||||
fun cleanObservers() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
@@ -11,6 +13,7 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
@@ -53,6 +56,10 @@ object LocalCache {
|
||||
// checkNotInMainThread()
|
||||
|
||||
return users[key] ?: run {
|
||||
require(isValidHex(key = key)) {
|
||||
"$key is not a valid hex"
|
||||
}
|
||||
|
||||
val newObject = User(key)
|
||||
users.putIfAbsent(key, newObject) ?: newObject
|
||||
}
|
||||
@@ -98,6 +105,10 @@ object LocalCache {
|
||||
checkNotInMainThread()
|
||||
|
||||
return notes.get(idHex) ?: run {
|
||||
require(isValidHex(idHex)) {
|
||||
"$idHex is not a valid hex"
|
||||
}
|
||||
|
||||
val newObject = Note(idHex)
|
||||
notes.putIfAbsent(idHex, newObject) ?: newObject
|
||||
}
|
||||
@@ -198,6 +209,14 @@ object LocalCache {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
|
||||
if (event.dTag() == "bookmark") {
|
||||
val loggedInUser = LocalPreferences.currentAccount()
|
||||
val hexKey = loggedInUser?.bechToBytes()
|
||||
if (hexKey != null) {
|
||||
val pubKey = Hex.encode(hexKey)
|
||||
if (pubKey == event.pubKey) {
|
||||
ExternalSignerUtils.content.remove(event.id)
|
||||
}
|
||||
}
|
||||
user.updateBookmark(event)
|
||||
}
|
||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
|
||||
@@ -807,23 +826,23 @@ object LocalCache {
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
|
||||
val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) }
|
||||
|
||||
if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) {
|
||||
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
|
||||
return
|
||||
}
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||
(
|
||||
(zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
|
||||
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
|
||||
)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
if (zapRequest == null) {
|
||||
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
|
||||
return
|
||||
}
|
||||
|
||||
// Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
repliesTo.forEach {
|
||||
@@ -1183,13 +1202,7 @@ object LocalCache {
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
// Doesn't need to clean up the replies and mentions.. Too small to matter.
|
||||
|
||||
// Counts the replies
|
||||
it.replyTo?.forEach { parent ->
|
||||
parent.removeReply(it)
|
||||
}
|
||||
removeFromCache(it)
|
||||
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
@@ -1208,12 +1221,7 @@ object LocalCache {
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
|
||||
// Counts the replies
|
||||
it.replyTo?.forEach { parent ->
|
||||
parent.removeReply(it)
|
||||
}
|
||||
removeFromCache(it)
|
||||
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
@@ -1240,21 +1248,12 @@ object LocalCache {
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
|
||||
val newerVersion = addressables[(it.event as? AddressableEvent)?.address()?.toTag()]
|
||||
if (newerVersion != null) {
|
||||
it.moveAllReferencesTo(newerVersion)
|
||||
}
|
||||
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
masterNote.removeBoost(it)
|
||||
masterNote.removeReaction(it)
|
||||
masterNote.removeZap(it)
|
||||
masterNote.clearEOSE() // allows reloading of these events
|
||||
}
|
||||
|
||||
removeFromCache(it)
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
@@ -1274,6 +1273,7 @@ object LocalCache {
|
||||
it.value.event is ReactionEvent || it.value.event is LnZapEvent || it.value.event is LnZapRequestEvent ||
|
||||
it.value.event is ReportEvent || it.value.event is GenericRepostEvent
|
||||
) &&
|
||||
it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
|
||||
it.value.liveSet?.isInUse() != true && // don't delete if observing.
|
||||
it.value.author?.pubkeyHex !in accounts && // don't delete if it is the logged in account
|
||||
it.value.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user
|
||||
@@ -1282,27 +1282,88 @@ object LocalCache {
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
masterNote.removeBoost(it)
|
||||
masterNote.removeReaction(it)
|
||||
masterNote.removeZap(it)
|
||||
masterNote.removeReport(it)
|
||||
masterNote.clearEOSE() // allows reloading of these events
|
||||
}
|
||||
|
||||
removeFromCache(it)
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
removeChildrenOf(childrenToBeRemoved)
|
||||
|
||||
toBeRemoved.forEach {
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.clearEOSE() // allows reloading of these events
|
||||
}
|
||||
}
|
||||
|
||||
if (toBeRemoved.size > 1) {
|
||||
println("PRUNE: ${toBeRemoved.size} thread replies removed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromCache(note: Note) {
|
||||
note.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(note)
|
||||
masterNote.removeBoost(note)
|
||||
masterNote.removeReaction(note)
|
||||
masterNote.removeZap(note)
|
||||
masterNote.removeReport(note)
|
||||
masterNote.clearEOSE() // allows reloading of these events if needed
|
||||
}
|
||||
|
||||
if (note.event is LnZapEvent) {
|
||||
(note.event as LnZapEvent).zappedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
if (note.event is LnZapRequestEvent) {
|
||||
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
if (note.event is ReportEvent) {
|
||||
(note.event as ReportEvent).reportedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it.key)
|
||||
author?.removeReport(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
|
||||
notes.remove(note.idHex)
|
||||
}
|
||||
|
||||
fun removeAuthorLinkTo(note: Note) {
|
||||
if (note.event is LnZapEvent) {
|
||||
(note.event as LnZapEvent).zappedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
if (note.event is LnZapRequestEvent) {
|
||||
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it)
|
||||
author?.removeZap(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
if (note.event is ReportEvent) {
|
||||
(note.event as ReportEvent).reportedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it.key)
|
||||
author?.removeReport(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChildrenOf(nextToBeRemoved: List<Note>) {
|
||||
nextToBeRemoved.forEach { note ->
|
||||
removeFromCache(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneExpiredEvents() {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -1313,17 +1374,7 @@ object LocalCache {
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
masterNote.removeBoost(it)
|
||||
masterNote.removeReaction(it)
|
||||
masterNote.removeZap(it)
|
||||
masterNote.removeReport(it)
|
||||
masterNote.clearEOSE() // allows reloading of these events
|
||||
}
|
||||
|
||||
removeFromCache(it)
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
@@ -1350,17 +1401,7 @@ object LocalCache {
|
||||
}.flatten()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
// Counts the replies
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
masterNote.removeBoost(it)
|
||||
masterNote.removeReaction(it)
|
||||
masterNote.removeZap(it)
|
||||
masterNote.removeReport(it)
|
||||
}
|
||||
|
||||
notes.remove(it.idHex)
|
||||
|
||||
removeFromCache(it)
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
@@ -1369,21 +1410,6 @@ object LocalCache {
|
||||
println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden")
|
||||
}
|
||||
|
||||
fun removeChildrenOf(nextToBeRemoved: List<Note>) {
|
||||
nextToBeRemoved.forEach { note ->
|
||||
if (note.event is LnZapEvent) {
|
||||
(note.event as LnZapEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) }
|
||||
}
|
||||
if (note.event is LnZapRequestEvent) {
|
||||
(note.event as LnZapRequestEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) }
|
||||
}
|
||||
if (note.event is ReportEvent) {
|
||||
(note.event as ReportEvent).reportedAuthor().mapNotNull { getUserIfExists(it.key)?.removeZap(note) }
|
||||
}
|
||||
notes.remove(note.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneContactLists(loggedIn: Set<HexKey>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -1476,9 +1502,10 @@ object LocalCache {
|
||||
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
||||
is LnZapEvent -> {
|
||||
event.zapRequest?.let {
|
||||
// must have a valid request
|
||||
verifyAndConsume(it, relay)
|
||||
consume(event)
|
||||
}
|
||||
consume(event)
|
||||
}
|
||||
is LnZapRequestEvent -> consume(event)
|
||||
is LnZapPaymentRequestEvent -> consume(event)
|
||||
|
||||
@@ -136,26 +136,60 @@ open class Note(val idHex: String) {
|
||||
.format(DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss"))
|
||||
}
|
||||
|
||||
data class LevelSignature(val signature: String, val createdAt: Long?, val author: User?)
|
||||
|
||||
/**
|
||||
* This method caches signatures during each execution to avoid recalculation in longer threads
|
||||
*/
|
||||
fun replyLevelSignature(
|
||||
eventsToConsider: Set<Note>,
|
||||
cachedSignatures: MutableMap<Note, String>
|
||||
): String {
|
||||
eventsToConsider: Set<HexKey>,
|
||||
cachedSignatures: MutableMap<Note, LevelSignature>,
|
||||
account: User,
|
||||
accountFollowingSet: Set<String>,
|
||||
now: Long
|
||||
): LevelSignature {
|
||||
val replyTo = replyTo
|
||||
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
|
||||
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||
return LevelSignature(
|
||||
signature = "/" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) + ";",
|
||||
createdAt = createdAt(),
|
||||
author = author
|
||||
)
|
||||
}
|
||||
|
||||
val mySignature = (
|
||||
val parent = (
|
||||
replyTo
|
||||
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
|
||||
.filter { it.idHex in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
|
||||
.map {
|
||||
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
|
||||
cachedSignatures[it] ?: it.replyLevelSignature(
|
||||
eventsToConsider,
|
||||
cachedSignatures,
|
||||
account,
|
||||
accountFollowingSet,
|
||||
now
|
||||
).apply { cachedSignatures.put(it, this) }
|
||||
}
|
||||
.maxByOrNull { it.length }?.removeSuffix(";") ?: ""
|
||||
) + "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||
.maxByOrNull { it.signature.length }
|
||||
)
|
||||
|
||||
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
|
||||
|
||||
val threadOrder = if (parent?.author == author && createdAt() != null) {
|
||||
// author of the thread first, in **ascending** order
|
||||
"9" + formattedDateTime((parent?.createdAt ?: 0) + (now - (createdAt() ?: 0))) + idHex.substring(0, 8)
|
||||
} else if (author?.pubkeyHex == account.pubkeyHex) {
|
||||
"8" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my replies
|
||||
} else if (author?.pubkeyHex in accountFollowingSet) {
|
||||
"7" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my follows replies.
|
||||
} else {
|
||||
"0" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // everyone else.
|
||||
}
|
||||
|
||||
val mySignature = LevelSignature(
|
||||
signature = parentSignature + "/" + threadOrder + ";",
|
||||
createdAt = createdAt(),
|
||||
author = author
|
||||
)
|
||||
|
||||
cachedSignatures[this] = mySignature
|
||||
return mySignature
|
||||
@@ -700,6 +734,12 @@ class NoteLiveSet(u: Note) {
|
||||
it.note.boosts.toImmutableList()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val relayInfo = innerRelays.map {
|
||||
it.note.relays.map {
|
||||
RelayBriefInfo(it)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
reactions.hasObservers() ||
|
||||
@@ -737,16 +777,12 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate() {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
postValue(NoteState(note))
|
||||
}
|
||||
postValue(NoteState(note))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,3 +818,10 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
|
||||
|
||||
@Immutable
|
||||
class NoteState(val note: Note)
|
||||
|
||||
@Immutable
|
||||
data class RelayBriefInfo(
|
||||
val url: String,
|
||||
val displayUrl: String = url.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/").intern(),
|
||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
|
||||
)
|
||||
|
||||
@@ -14,4 +14,6 @@ data class RelaySetupInfo(
|
||||
val spamCount: Int = 0,
|
||||
val feedTypes: Set<FeedType>,
|
||||
val paidRelay: Boolean = false
|
||||
)
|
||||
) {
|
||||
val briefInfo: RelayBriefInfo = RelayBriefInfo(url)
|
||||
}
|
||||
|
||||
@@ -5,16 +5,15 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.quartz.encoders.Bech32
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
@@ -27,9 +26,6 @@ import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.math.BigDecimal
|
||||
import java.util.regex.Pattern
|
||||
|
||||
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
|
||||
|
||||
@Stable
|
||||
class User(val pubkeyHex: String) {
|
||||
@@ -104,6 +100,10 @@ class User(val pubkeyHex: String) {
|
||||
liveSet?.innerBookmarks?.invalidateData()
|
||||
}
|
||||
|
||||
fun clearEOSE() {
|
||||
latestEOSEs = emptyMap()
|
||||
}
|
||||
|
||||
fun updateContactList(event: ContactListEvent) {
|
||||
if (event.id == latestContactList?.id) return
|
||||
|
||||
@@ -264,19 +264,11 @@ class User(val pubkeyHex: String) {
|
||||
info?.updatedMetadataAt = latestMetadata.createdAt
|
||||
info?.tags = latestMetadata.tags.toImmutableListOfLists()
|
||||
|
||||
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
|
||||
try {
|
||||
val url = String(Bech32.decodeBytes(newUserInfo.lud06!!, false).second)
|
||||
|
||||
val matcher = lnurlpPattern.matcher(url)
|
||||
while (matcher.find()) {
|
||||
val domain = matcher.group(2)
|
||||
val username = matcher.group(3)
|
||||
|
||||
info?.lud16 = "$username@$domain"
|
||||
if (newUserInfo.lud16.isNullOrBlank()) {
|
||||
info?.lud06?.let {
|
||||
if (it.lowercase().startsWith("lnurl")) {
|
||||
info?.lud16 = Lud06().toLud16(it)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
// Doesn't create errors.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,15 +457,12 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate() {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
postValue(UserState(user))
|
||||
}
|
||||
postValue(UserState(user))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SignerType {
|
||||
SIGN_EVENT,
|
||||
NIP04_ENCRYPT,
|
||||
NIP04_DECRYPT,
|
||||
NIP44_ENCRYPT,
|
||||
NIP44_DECRYPT,
|
||||
GET_PUBLIC_KEY,
|
||||
DECRYPT_ZAP_EVENT
|
||||
}
|
||||
|
||||
object ExternalSignerUtils {
|
||||
val content = LruCache<String, String>(10)
|
||||
var isActivityRunning: Boolean = false
|
||||
val cachedDecryptedContent = mutableMapOf<HexKey, String>()
|
||||
lateinit var account: Account
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var decryptResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var blockListResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun requestRejectedToast() {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.sign_request_rejected),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun default() {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun start(activity: MainActivity) {
|
||||
activityResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
decryptResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
cachedDecryptedContent[id] = event
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
blockListResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
cachedDecryptedContent[id] = decryptedContent
|
||||
account.live.invalidateData()
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openSigner(
|
||||
data: String,
|
||||
type: SignerType,
|
||||
intentResult: ActivityResultLauncher<Intent>,
|
||||
pubKey: HexKey,
|
||||
id: String
|
||||
) {
|
||||
try {
|
||||
ServiceManager.shouldPauseService = false
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
|
||||
val signerType = when (type) {
|
||||
SignerType.SIGN_EVENT -> "sign_event"
|
||||
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
|
||||
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
|
||||
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
|
||||
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
|
||||
SignerType.GET_PUBLIC_KEY -> "get_public_key"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
|
||||
}
|
||||
intent.putExtra("type", signerType)
|
||||
intent.putExtra("pubKey", pubKey)
|
||||
intent.putExtra("id", id)
|
||||
if (type !== SignerType.GET_PUBLIC_KEY) {
|
||||
intent.putExtra("current_user", account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
intent.`package` = "com.greenart7c3.nostrsigner"
|
||||
intentResult.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.error_opening_external_signer),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSigner(event: EventInterface, columnName: String = "signature") {
|
||||
checkNotInMainThread()
|
||||
|
||||
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
|
||||
if (result !== null) {
|
||||
content.put(event.id(), result)
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.shouldPauseService = false
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.SIGN_EVENT,
|
||||
activityResultLauncher,
|
||||
"",
|
||||
event.id()
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
blockListResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
|
||||
val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) {
|
||||
data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray()
|
||||
} else {
|
||||
data
|
||||
}
|
||||
|
||||
Amethyst.instance.contentResolver.query(
|
||||
Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"),
|
||||
localData,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
if (it !== null) {
|
||||
if (it.moveToFirst()) {
|
||||
val index = it.getColumnIndex(columnName)
|
||||
return it.getString(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
return
|
||||
}
|
||||
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
decryptedContent,
|
||||
signerType,
|
||||
activityResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZapEvent(event: LnZapRequestEvent) {
|
||||
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
|
||||
if (result !== null) {
|
||||
content.put(event.id, result)
|
||||
cachedDecryptedContent[event.id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.DECRYPT_ZAP_EVENT,
|
||||
decryptResultLauncher,
|
||||
event.pubKey,
|
||||
event.id
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,26 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
@@ -143,6 +152,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
event.cachedGift(privateKey)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +175,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
event.cachedGossip(privateKey)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't store sealed gossips to avoid rebroadcasting by mistake.
|
||||
@@ -181,7 +218,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
|
||||
if (this::account.isInitialized) {
|
||||
val event = account.createAuthEvent(relay, challenge)
|
||||
|
||||
if (event != null) {
|
||||
Client.send(
|
||||
event,
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.Error
|
||||
|
||||
abstract class NostrDataSource(val debugName: String) {
|
||||
@@ -23,6 +24,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
data class Counter(var counter: Int)
|
||||
|
||||
private var eventCounter = mapOf<String, Counter>()
|
||||
var changingFilters = AtomicBoolean()
|
||||
|
||||
fun printCounter() {
|
||||
eventCounter.forEach {
|
||||
@@ -49,7 +51,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
// Log.e("ERROR", "Relay ${relay.url}: ${error.message}")
|
||||
}
|
||||
|
||||
override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
|
||||
override fun onRelayStateChange(type: Relay.Type, relay: Relay, subscriptionId: String?) {
|
||||
// Log.d("RELAY", "Relay ${relay.url} ${when (type) {
|
||||
// Relay.Type.CONNECT -> "connected."
|
||||
// Relay.Type.DISCONNECT -> "disconnected."
|
||||
@@ -57,13 +59,19 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
// Relay.Type.EOSE -> "sent all events it had stored."
|
||||
// }}")
|
||||
|
||||
if (type == Relay.Type.EOSE && channel != null) {
|
||||
if (type == Relay.Type.EOSE && subscriptionId != null && subscriptionId in subscriptions.keys) {
|
||||
// updates a per subscripton since date
|
||||
subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
|
||||
subscriptions[subscriptionId]?.updateEOSE(
|
||||
TimeUtils.fiveMinutesAgo(), // in case people's clock is slighly off.
|
||||
relay.url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
|
||||
if (success) {
|
||||
markAsSeenOnRelay(eventId, relay)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuth(relay: Relay, challenge: String) {
|
||||
@@ -136,6 +144,8 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
// saves the current content to only update if it changes
|
||||
val currentFilters = activeSubscriptions.associate { it.id to it.toJson() }
|
||||
|
||||
changingFilters.getAndSet(true)
|
||||
|
||||
updateChannelFilters()
|
||||
|
||||
// Makes sure to only send an updated filter when it actually changes.
|
||||
@@ -167,12 +177,18 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changingFilters.getAndSet(false)
|
||||
}
|
||||
|
||||
open fun consume(event: Event, relay: Relay) {
|
||||
LocalCache.verifyAndConsume(event, relay)
|
||||
}
|
||||
|
||||
open fun markAsSeenOnRelay(eventId: String, relay: Relay) {
|
||||
LocalCache.getNoteIfExists(eventId)?.addRelay(relay)
|
||||
}
|
||||
|
||||
abstract fun updateChannelFilters()
|
||||
open fun auth(relay: Relay, challenge: String) = Unit
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
}
|
||||
|
||||
fun createFollowAccountsFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followSet = follows.plus(account.userProfile().pubkeyHex).toList()
|
||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
|
||||
@@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
override fun auth(relay: Relay, challenge: String) {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey)
|
||||
val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
|
||||
Client.send(
|
||||
event,
|
||||
relay.url
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object PackageUtils {
|
||||
private fun isPackageInstalled(context: Context, target: String): Boolean {
|
||||
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
|
||||
}
|
||||
|
||||
fun isOrbotInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "org.torproject.android")
|
||||
}
|
||||
|
||||
fun isAmberInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.round
|
||||
|
||||
class ZapPaymentHandler(val account: Account) {
|
||||
|
||||
@Immutable
|
||||
data class Payable(
|
||||
val info: ZapSplitSetup,
|
||||
val user: User?,
|
||||
val amountMilliSats: Long,
|
||||
val invoice: String
|
||||
)
|
||||
|
||||
suspend fun zap(
|
||||
note: Note,
|
||||
amountMilliSats: Long,
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
||||
zapType: LnZapEvent.ZapType
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val zapSplitSetup = note.event?.zapSplitSetup()
|
||||
|
||||
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
|
||||
zapSplitSetup
|
||||
} else {
|
||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
||||
return@withContext
|
||||
}
|
||||
|
||||
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
|
||||
}
|
||||
|
||||
val totalWeight = zapsToSend.sumOf { it.weight }
|
||||
|
||||
val invoicesToPayOnIntent = mutableListOf<Payable>()
|
||||
|
||||
zapsToSend.forEachIndexed { index, value ->
|
||||
val outerProgressMin = index / zapsToSend.size.toFloat()
|
||||
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
|
||||
|
||||
val zapValue =
|
||||
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
|
||||
|
||||
if (value.isLnAddress) {
|
||||
innerZap(
|
||||
lud16 = value.lnAddressOrPubKeyHex,
|
||||
note = note,
|
||||
amount = zapValue,
|
||||
pollOption = pollOption,
|
||||
message = message,
|
||||
context = context,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
||||
},
|
||||
zapType = zapType,
|
||||
onPayInvoiceThroughIntent = {
|
||||
invoicesToPayOnIntent.add(
|
||||
Payable(
|
||||
info = value,
|
||||
user = null,
|
||||
amountMilliSats = zapValue,
|
||||
invoice = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
|
||||
val lud16 = user?.info?.lnAddress()
|
||||
|
||||
if (lud16 != null) {
|
||||
innerZap(
|
||||
lud16 = lud16,
|
||||
note = note,
|
||||
amount = zapValue,
|
||||
pollOption = pollOption,
|
||||
message = message,
|
||||
context = context,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
|
||||
},
|
||||
zapType = zapType,
|
||||
overrideUser = user,
|
||||
onPayInvoiceThroughIntent = {
|
||||
invoicesToPayOnIntent.add(
|
||||
Payable(
|
||||
info = value,
|
||||
user = user,
|
||||
amountMilliSats = zapValue,
|
||||
invoice = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
onError(
|
||||
context.getString(
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invoicesToPayOnIntent.isNotEmpty()) {
|
||||
onPayViaIntent(invoicesToPayOnIntent.toImmutableList())
|
||||
onProgress(1f)
|
||||
} else {
|
||||
launch(Dispatchers.IO) {
|
||||
// Awaits for the event to come back to LocalCache.
|
||||
delay(5000)
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerZap(
|
||||
lud16: String,
|
||||
note: Note,
|
||||
amount: Long,
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayInvoiceThroughIntent: (String) -> Unit,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null
|
||||
) {
|
||||
var zapRequestJson = ""
|
||||
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
|
||||
if (zapRequest != null) {
|
||||
zapRequestJson = zapRequest.toJson()
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(0.10f)
|
||||
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount,
|
||||
message,
|
||||
zapRequestJson,
|
||||
onSuccess = {
|
||||
onProgress(0.7f)
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = it,
|
||||
note,
|
||||
onResponse = { response ->
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
)
|
||||
onProgress(0.8f)
|
||||
} else {
|
||||
try {
|
||||
onPayInvoiceThroughIntent(it)
|
||||
} catch (e: Exception) {
|
||||
onError(context.getString(R.string.lightning_wallets_not_found2))
|
||||
}
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,14 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.Bech32
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import com.vitorpamplona.quartz.encoders.toLnUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.net.URLEncoder
|
||||
|
||||
class LightningAddressResolver() {
|
||||
@@ -27,11 +25,7 @@ class LightningAddressResolver() {
|
||||
}
|
||||
|
||||
if (lnaddress.lowercase().startsWith("lnurl")) {
|
||||
return try {
|
||||
String(Bech32.decodeBytes(lnaddress, false).second)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
return Lud06().toLnUrlp(lnaddress)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -57,16 +51,16 @@ class LightningAddressResolver() {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server up and if the lightning address is correct")
|
||||
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
onError("Could not resolve $url. Check if the server is up and if the lightning address $lnaddress is correct")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
@@ -82,22 +76,13 @@ class LightningAddressResolver() {
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(response.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
}
|
||||
}
|
||||
client.newCall(request).execute().use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
@@ -115,7 +100,7 @@ class LightningAddressResolver() {
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: (String) -> Unit,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
) {
|
||||
@@ -159,14 +144,22 @@ class LightningAddressResolver() {
|
||||
|
||||
lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr ->
|
||||
// Forces LN Invoice amount to be the requested amount.
|
||||
val expectedAmountInSats = BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
|
||||
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
|
||||
if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) {
|
||||
if (invoiceAmount.toLong() == expectedAmountInSats) {
|
||||
onProgress(0.7f)
|
||||
onSuccess(pr)
|
||||
} else {
|
||||
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server")
|
||||
onProgress(0.0f)
|
||||
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
|
||||
}
|
||||
} ?: onError("Invoice Not Created (element pr not found in the resulting JSON)")
|
||||
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
|
||||
onProgress(0.0f)
|
||||
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
|
||||
} ?: run {
|
||||
onProgress(0.0f)
|
||||
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
|
||||
}
|
||||
},
|
||||
onError = onError
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.SignerType
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
@@ -19,43 +21,127 @@ import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
suspend fun consume(event: GiftWrapEvent) {
|
||||
if (!LocalCache.justVerify(event)) return
|
||||
if (!notificationManager().areNotificationsEnabled()) return
|
||||
|
||||
suspend fun consume(event: Event) {
|
||||
if (LocalCache.notes[event.id] == null) {
|
||||
if (LocalCache.justVerify(event)) {
|
||||
LocalCache.justConsume(event, null)
|
||||
// PushNotification Wraps don't include a receiver.
|
||||
// Test with all logged in accounts
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) {
|
||||
consumeIfMatchesAccount(event, acc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val manager = notificationManager()
|
||||
if (manager.areNotificationsEnabled()) {
|
||||
when (event) {
|
||||
is PrivateDmEvent -> notify(event)
|
||||
is LnZapEvent -> notify(event)
|
||||
is GiftWrapEvent -> unwrapAndNotify(event)
|
||||
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
|
||||
val key = account.keyPair.privKey
|
||||
if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
pushWrappedEvent.content,
|
||||
pushWrappedEvent.pubKey,
|
||||
pushWrappedEvent.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: ""
|
||||
}
|
||||
pushWrappedEvent.unwrap(cached)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key != null) {
|
||||
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unwrapAndConsume(event: Event, account: Account): Event? {
|
||||
private fun unwrapAndConsume(event: Event, account: Account): Event? {
|
||||
if (!LocalCache.justVerify(event)) return null
|
||||
|
||||
return when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
val key = account.keyPair.privKey ?: return null
|
||||
event.cachedGift(key)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGift(key)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
val key = account.keyPair.privKey ?: return null
|
||||
event.cachedGossip(key)?.let {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGossip(key)?.let {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -65,106 +151,106 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unwrapAndNotify(giftWrap: GiftWrapEvent) {
|
||||
val giftWrapNote = LocalCache.notes[giftWrap.id] ?: return
|
||||
private fun notify(event: ChatMessageEvent, acc: Account) {
|
||||
if (event.createdAt > TimeUtils.fiveMinutesAgo() && // old event being re-broadcasted
|
||||
event.pubKey != acc.userProfile().pubkeyHex
|
||||
) { // from the user
|
||||
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
val chatNote = LocalCache.notes[event.id] ?: return
|
||||
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
|
||||
|
||||
if (acc != null && acc.userProfile().pubkeyHex == giftWrap.recipientPubKey()) {
|
||||
val chatEvent = unwrapAndConsume(giftWrap, account = acc)
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
|
||||
if (chatEvent is ChatMessageEvent && acc.keyPair.privKey != null) {
|
||||
val chatNote = LocalCache.notes[chatEvent.id] ?: return
|
||||
val chatRoom = chatEvent.chatroomKey(acc.keyPair.pubKey.toHexKey())
|
||||
val isKnownRoom = (
|
||||
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(chatRoom)
|
||||
) && !acc.isAllHidden(chatRoom.users)
|
||||
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
|
||||
val isKnownRoom = (
|
||||
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(chatRoom)
|
||||
) && !acc.isAllHidden(chatRoom.users)
|
||||
|
||||
if (isKnownRoom) {
|
||||
val content = chatNote.event?.content() ?: ""
|
||||
val user = chatNote.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = chatNote.author?.profilePicture()
|
||||
val noteUri = chatNote.toNEvent()
|
||||
notificationManager().sendDMNotification(chatEvent.id, content, user, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
if (isKnownRoom) {
|
||||
val content = chatNote.event?.content() ?: ""
|
||||
val user = chatNote.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = chatNote.author?.profilePicture()
|
||||
val noteUri = chatNote.toNEvent()
|
||||
notificationManager().sendDMNotification(
|
||||
event.id,
|
||||
content,
|
||||
user,
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(event: PrivateDmEvent) {
|
||||
private fun notify(event: PrivateDmEvent, acc: Account) {
|
||||
val note = LocalCache.notes[event.id] ?: return
|
||||
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
||||
|
||||
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
|
||||
val followingKeySet = acc.followingKeySet()
|
||||
|
||||
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
|
||||
(
|
||||
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(it)
|
||||
) && !acc.isAllHidden(it.users)
|
||||
}.toSet()
|
||||
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
|
||||
(
|
||||
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
|
||||
acc.userProfile().hasSentMessagesTo(it)
|
||||
) && !acc.isAllHidden(it.users)
|
||||
}.toSet()
|
||||
|
||||
note.author?.let {
|
||||
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
|
||||
val content = acc.decryptContent(note) ?: ""
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
note.author?.let {
|
||||
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
|
||||
val content = acc.decryptContent(note) ?: ""
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(event: LnZapEvent) {
|
||||
private fun notify(event: LnZapEvent, acc: Account) {
|
||||
val noteZapEvent = LocalCache.notes[event.id] ?: return
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
// old event being re-broadcast
|
||||
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
|
||||
|
||||
if (acc != null && acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
|
||||
val amount = showAmount(event.amount)
|
||||
val senderInfo = (noteZapRequest?.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
|
||||
if (decryptedContent != null) {
|
||||
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
Pair(author, decryptedContent.content)
|
||||
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
|
||||
Pair(noteZapRequest.author, noteZapRequest.event?.content())
|
||||
} else {
|
||||
Pair(noteZapRequest.author, null)
|
||||
}
|
||||
if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
|
||||
val amount = showAmount(event.amount)
|
||||
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
|
||||
if (decryptedContent != null) {
|
||||
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
Pair(author, decryptedContent.content)
|
||||
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
|
||||
Pair(noteZapRequest.author, noteZapRequest.event?.content())
|
||||
} else {
|
||||
Pair(noteZapRequest.author, null)
|
||||
}
|
||||
|
||||
val zappedContent =
|
||||
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
|
||||
|
||||
val user = senderInfo?.first?.toBestDisplayName() ?: ""
|
||||
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo?.second?.ifBlank { null }?.let {
|
||||
title += " ($it)"
|
||||
}
|
||||
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
|
||||
zappedContent?.let {
|
||||
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
|
||||
val zappedContent =
|
||||
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
|
||||
|
||||
val user = senderInfo?.first?.toBestDisplayName() ?: ""
|
||||
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo?.second?.ifBlank { null }?.let {
|
||||
title += " ($it)"
|
||||
}
|
||||
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
|
||||
zappedContent?.let {
|
||||
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class RegisterAccounts(
|
||||
): List<RelayAuthEvent> {
|
||||
return accounts.mapNotNull {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
if (acc != null) {
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
|
||||
if (relayToUse != null) {
|
||||
acc.createAuthEvent(relayToUse, notificationToken)
|
||||
@@ -36,7 +36,7 @@ class RegisterAccounts(
|
||||
}
|
||||
}
|
||||
|
||||
private fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||
try {
|
||||
val jsonObject = """{
|
||||
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
@@ -22,6 +23,7 @@ import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
|
||||
@@ -50,6 +52,8 @@ import java.nio.charset.StandardCharsets
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ExternalSignerUtils.start(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
LocalPreferences.migrateSingleUserPrefs()
|
||||
@@ -65,7 +69,10 @@ class MainActivity : AppCompatActivity() {
|
||||
themeViewModel.onChange(LocalPreferences.getTheme())
|
||||
AmethystTheme(themeViewModel) {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel(this@MainActivity)
|
||||
}
|
||||
@@ -81,7 +88,8 @@ class MainActivity : AppCompatActivity() {
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||
val connectivityManager =
|
||||
getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||
connectivityManager.requestNetwork(networkRequest, networkCallback)
|
||||
}
|
||||
|
||||
@@ -94,11 +102,13 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.start(this@MainActivity)
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.start(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +118,9 @@ class MainActivity : AppCompatActivity() {
|
||||
debugState(this)
|
||||
// }
|
||||
|
||||
ServiceManager.pause()
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -140,8 +152,10 @@ class MainActivity : AppCompatActivity() {
|
||||
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(this@MainActivity)
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +167,8 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasMobileData =
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
|
||||
@@ -170,7 +185,9 @@ class MainActivity : AppCompatActivity() {
|
||||
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.pause()
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,9 +195,15 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
class GetMediaActivityResultContract : ActivityResultContracts.GetContent() {
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
// Force only images and videos to be selectable
|
||||
// Force OPEN Document because of the resulting URI must be passed to the
|
||||
// Playback service and the picker's permissions only allow the activity to read the URI
|
||||
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
// Force only images and videos to be selectable
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,24 +92,32 @@ open class NewMediaModel : ViewModel() {
|
||||
} else {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Uploading"
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = serverToUse,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
createNIP94Record(imageUrl, mimeType, description, sensitiveContent, relayList = relayList)
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = serverToUse,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
createNIP94Record(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
sensitiveContent,
|
||||
relayList = relayList
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,37 +101,50 @@ class NewMessageTagger(
|
||||
|
||||
key = key.removePrefix("@")
|
||||
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val keyB32 = key.substring(0, 63)
|
||||
val restOfWord = key.substring(63)
|
||||
|
||||
if (key.startsWith("nsec1", true)) {
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
}
|
||||
|
||||
val keyB32 = key.substring(0, 63)
|
||||
val restOfWord = key.substring(63)
|
||||
// Converts to npub
|
||||
val pubkey = Nip19.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
} else if (key.startsWith("npub1", true)) {
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
}
|
||||
|
||||
val keyB32 = key.substring(0, 63)
|
||||
val restOfWord = key.substring(63)
|
||||
|
||||
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
} else if (key.startsWith("note1", true)) {
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
}
|
||||
|
||||
val keyB32 = key.substring(0, 63)
|
||||
val restOfWord = key.substring(63)
|
||||
|
||||
val noteId = Nip19.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(noteId, restOfWord)
|
||||
} else if (key.startsWith("nprofile", true)) {
|
||||
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
val pubkeyRelay = Nip19.uriToRoute(key) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
|
||||
} else if (key.startsWith("nevent1", true)) {
|
||||
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
val noteRelayId = Nip19.uriToRoute(key) ?: return null
|
||||
|
||||
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
|
||||
} else if (key.startsWith("naddr1", true)) {
|
||||
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
val address = Nip19.uriToRoute(key) ?: return null
|
||||
|
||||
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.util.Size
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -82,20 +83,24 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||
import com.vitorpamplona.amethyst.ui.components.*
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.PollIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
@@ -111,6 +116,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Math.round
|
||||
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
@@ -133,13 +139,7 @@ fun NewPostView(
|
||||
var showRelaysDialog by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var relayList = account.activeRelays()?.filter {
|
||||
it.write
|
||||
}?.map {
|
||||
it
|
||||
} ?: account.convertLocalRelays().filter {
|
||||
it.write
|
||||
}
|
||||
var relayList = account.activeWriteRelays()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(account, baseReplyTo, quote)
|
||||
@@ -169,7 +169,7 @@ fun NewPostView(
|
||||
) {
|
||||
if (showRelaysDialog) {
|
||||
RelaySelectionDialog(
|
||||
list = relayList,
|
||||
preSelectedList = relayList,
|
||||
onClose = {
|
||||
showRelaysDialog = false
|
||||
},
|
||||
@@ -327,9 +327,9 @@ fun NewPostView(
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp)
|
||||
) {
|
||||
FowardZapTo(postViewModel)
|
||||
FowardZapTo(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +727,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FowardZapTo(postViewModel: NewPostViewModel) {
|
||||
fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@@ -761,7 +761,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.zap_forward_title),
|
||||
text = stringResource(R.string.zap_split_title),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
@@ -771,22 +771,52 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
|
||||
Divider()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.zap_forward_explainer),
|
||||
text = stringResource(R.string.zap_split_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
||||
postViewModel.forwardZapTo.items.forEachIndexed { index, splitItem ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
|
||||
BaseUserPicture(splitItem.key, Size55dp, accountViewModel = accountViewModel)
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
UsernameDisplay(splitItem.key, showPlayButton = false)
|
||||
Text(
|
||||
text = String.format("%.0f%%", splitItem.percentage * 100),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Slider(
|
||||
value = splitItem.percentage,
|
||||
onValueChange = { sliderValue ->
|
||||
val rounded = (round(sliderValue * 20)) / 20.0f
|
||||
postViewModel.updateZapPercentage(index, rounded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = postViewModel.forwardZapToEditting,
|
||||
onValueChange = {
|
||||
postViewModel.updateZapForwardTo(it)
|
||||
},
|
||||
|
||||
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
|
||||
label = { Text(text = stringResource(R.string.zap_split_serarch_and_add_user)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.zap_forward_lnAddress),
|
||||
text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
@@ -1376,8 +1406,8 @@ fun ImageVideoDescription(
|
||||
try {
|
||||
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
|
||||
} catch (e: Exception) {
|
||||
onError("Unable to load file")
|
||||
Log.e("NewPostView", "Couldn't create thumbnail for $uri")
|
||||
onError("Unable to load thumbnail")
|
||||
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.ui.components.Split
|
||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -88,9 +90,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
var wantsInvoice by mutableStateOf(false)
|
||||
|
||||
data class ForwardZapSetup(val user: User) {
|
||||
var percentage by mutableStateOf(100)
|
||||
}
|
||||
|
||||
// Forward Zap to
|
||||
var wantsForwardZapTo by mutableStateOf(false)
|
||||
var forwardZapTo by mutableStateOf<User?>(null)
|
||||
var forwardZapTo by mutableStateOf<Split<User>>(Split())
|
||||
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
// NSFW, Sensitive
|
||||
@@ -151,7 +157,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
wantsToAddGeoHash = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
forwardZapTo = null
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
this.account = account
|
||||
@@ -166,10 +172,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
val dmUsers = toUsersTagger.mentions
|
||||
|
||||
val zapReceiver = if (wantsForwardZapTo) {
|
||||
if (forwardZapTo != null) {
|
||||
forwardZapTo?.info?.lud16 ?: forwardZapTo?.info?.lud06
|
||||
} else {
|
||||
forwardZapToEditting.text
|
||||
forwardZapTo?.items?.map {
|
||||
ZapSplitSetup(
|
||||
lnAddressOrPubKeyHex = it.key.pubkeyHex,
|
||||
relay = it.key.relaysBeingUsed.keys.firstOrNull(),
|
||||
weight = it.percentage.toDouble(),
|
||||
isLnAddress = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -355,7 +364,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
wantsToAddGeoHash = false
|
||||
forwardZapTo = null
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
userSuggestions = emptyList()
|
||||
@@ -427,10 +436,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
open fun updateZapForwardTo(it: TextFieldValue) {
|
||||
forwardZapToEditting = it
|
||||
if (it.selection.collapsed) {
|
||||
val lastWord = it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
val lastWord = it.text
|
||||
userSuggestionAnchor = it.selection
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
if (lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
@@ -460,6 +469,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
|
||||
)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo?.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
/*
|
||||
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
val lastWordStart = it.end - lastWord.length
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
@@ -468,7 +480,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
forwardZapToEditting = TextFieldValue(
|
||||
forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert),
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
|
||||
)
|
||||
)*/
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
val lastWordStart = it.end - lastWord.length
|
||||
@@ -656,6 +668,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
isValidvalueMaximum.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun updateZapPercentage(index: Int, sliderValue: Float) {
|
||||
forwardZapTo?.updatePercentage(index, sliderValue)
|
||||
}
|
||||
}
|
||||
|
||||
enum class GeohashPrecision(val digits: Int) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -37,6 +38,7 @@ import kotlinx.coroutines.withContext
|
||||
fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
val postViewModel: NewUserMetadataViewModel = viewModel()
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(account)
|
||||
@@ -82,7 +84,9 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(1f),
|
||||
|
||||
@@ -124,7 +124,6 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
|
||||
}
|
||||
|
||||
clear()
|
||||
}
|
||||
|
||||
@@ -183,23 +182,25 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
contentResolver.getType(galleryUri),
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
onUploading(false)
|
||||
onUploaded(imageUrl)
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
onUploading(false)
|
||||
onUploaded(imageUrl)
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlin.math.abs
|
||||
|
||||
class SplitItem<T>(val key: T) {
|
||||
var percentage by mutableStateOf(0f)
|
||||
}
|
||||
|
||||
class Split<T>() {
|
||||
var items: List<SplitItem<T>> by mutableStateOf(emptyList())
|
||||
|
||||
fun addItem(key: T): Int {
|
||||
val wasEqualSplit = isEqualSplit()
|
||||
val newItem = SplitItem(key)
|
||||
items = items.plus(newItem)
|
||||
|
||||
if (wasEqualSplit) {
|
||||
forceEqualSplit()
|
||||
} else {
|
||||
updatePercentage(items.lastIndex, equalSplit())
|
||||
}
|
||||
|
||||
return items.lastIndex
|
||||
}
|
||||
|
||||
fun equalSplit() = 1f / items.size
|
||||
|
||||
fun isEqualSplit(): Boolean {
|
||||
val expectedPercentage = equalSplit()
|
||||
return items.all { (it.percentage - expectedPercentage) < 0.01 }
|
||||
}
|
||||
|
||||
fun forceEqualSplit() {
|
||||
val correctPercentage = equalSplit()
|
||||
items.forEach {
|
||||
it.percentage = correctPercentage
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePercentage(index: Int, percentage: Float) {
|
||||
if (items.isEmpty()) return
|
||||
|
||||
val splitItem = items.getOrNull(index) ?: return
|
||||
|
||||
if (items.size == 1) {
|
||||
splitItem.percentage = 1f
|
||||
} else {
|
||||
splitItem.percentage = percentage
|
||||
|
||||
println("Update ${items[index].key} to $percentage")
|
||||
|
||||
val othersMustShare = 1.0f - splitItem.percentage
|
||||
|
||||
val othersHave = items.sumOf {
|
||||
if (it == splitItem) 0.0 else it.percentage.toDouble()
|
||||
}.toFloat()
|
||||
|
||||
if (abs(othersHave - othersMustShare) < 0.01) return // nothing to do
|
||||
|
||||
println("Others Must Share $othersMustShare but have $othersHave")
|
||||
|
||||
bottomUpAdjustment(othersMustShare, othersHave, index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomUpAdjustment(othersMustShare: Float, othersHave: Float, exceptForIndex: Int) {
|
||||
var needToRemove = othersHave - othersMustShare
|
||||
if (needToRemove > 0) {
|
||||
for (i in items.indices.reversed()) {
|
||||
if (i == exceptForIndex) continue // do not update the current item
|
||||
|
||||
if (needToRemove < items[i].percentage) {
|
||||
val oldValue = items[i].percentage
|
||||
items[i].percentage -= needToRemove
|
||||
needToRemove = 0f
|
||||
println("- Updating ${items[i].key} from $oldValue to ${items[i].percentage - needToRemove}. $needToRemove left")
|
||||
} else {
|
||||
val oldValue = items[i].percentage
|
||||
needToRemove -= items[i].percentage
|
||||
items[i].percentage = 0f
|
||||
println("- Updating ${items[i].key} from $oldValue to ${0}. $needToRemove left")
|
||||
}
|
||||
|
||||
if (needToRemove < 0.01) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (needToRemove < 0) {
|
||||
if (items.lastIndex == exceptForIndex) {
|
||||
items[items.lastIndex - 1].percentage += -needToRemove
|
||||
} else {
|
||||
items.last().percentage += -needToRemove
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,6 +704,7 @@ fun ControlWhenPlayerIsActive(
|
||||
|
||||
controller.addListener(listener)
|
||||
onDispose {
|
||||
view.keepScreenOn = false
|
||||
controller.removeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
@@ -12,18 +14,44 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val privKey = account.keyPair.privKey ?: return emptyList()
|
||||
|
||||
val bookmarks = account.userProfile().latestBookmarkList
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(privKey)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
if (account.loginWithExternalSigner) {
|
||||
val id = bookmarks?.id
|
||||
if (id != null) {
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
|
||||
if (decryptedContent == null) {
|
||||
ExternalSignerUtils.decryptBookmark(
|
||||
bookmarks.content,
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
id
|
||||
)
|
||||
} else {
|
||||
bookmarks.decryptedContent = decryptedContent
|
||||
}
|
||||
}
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: ""
|
||||
|
||||
val addresses = bookmarks?.privateTaggedAddresses(privKey)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
val notes = bookmarks?.privateTaggedEvents(decryptedContent)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
val addresses = bookmarks?.privateTaggedAddresses(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
} else {
|
||||
val privKey = account.keyPair.privKey ?: return emptyList()
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(privKey)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
|
||||
val addresses = bookmarks?.privateTaggedAddresses(privKey)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,18 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return account.getBlockList()
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(account.keyPair.privKey)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -63,7 +64,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
@@ -72,11 +72,12 @@ import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
@@ -109,7 +110,7 @@ fun DrawerContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 25.dp)
|
||||
.padding(top = 100.dp),
|
||||
.padding(top = 70.dp),
|
||||
scaffoldState,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -162,7 +163,7 @@ fun ProfileContent(
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.height(120.dp)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
@@ -171,7 +172,7 @@ fun ProfileContent(
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.height(120.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,7 +221,6 @@ fun ProfileContent(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(top = 15.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
nav(route)
|
||||
@@ -455,7 +455,6 @@ fun ListContent(
|
||||
}
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||
var wantsToEditRelays by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -491,7 +490,7 @@ fun ListContent(
|
||||
)
|
||||
|
||||
IconRowRelays(
|
||||
relayViewModel = relayViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
@@ -634,27 +633,37 @@ private fun enableTor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayStatus(
|
||||
relayViewModel: RelayPoolViewModel
|
||||
) {
|
||||
val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--")
|
||||
val isConnected by relayViewModel.isConnected.observeAsState(false)
|
||||
private fun RelayStatus(accountViewModel: AccountViewModel) {
|
||||
val connectedRelaysText by RelayPool.statusFlow.collectAsState(initial = RelayPoolStatus(0, 0))
|
||||
|
||||
RenderRelayStatus(connectedRelaysText, isConnected)
|
||||
RenderRelayStatus(connectedRelaysText)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRelayStatus(
|
||||
connectedRelaysText: String,
|
||||
isConnected: Boolean
|
||||
relayPool: RelayPoolStatus
|
||||
) {
|
||||
val text by remember(relayPool) {
|
||||
derivedStateOf {
|
||||
"${relayPool.connected}/${relayPool.available}"
|
||||
}
|
||||
}
|
||||
|
||||
val placeHolder = MaterialTheme.colors.placeholderText
|
||||
|
||||
val color by remember(relayPool) {
|
||||
derivedStateOf {
|
||||
if (relayPool.isConnected) {
|
||||
placeHolder
|
||||
} else {
|
||||
Color.Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = connectedRelaysText,
|
||||
color = if (isConnected) {
|
||||
MaterialTheme.colors.placeholderText
|
||||
} else {
|
||||
Color.Red
|
||||
},
|
||||
text = text,
|
||||
color = color,
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
}
|
||||
@@ -710,7 +719,7 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
|
||||
fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -737,7 +746,7 @@ fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
|
||||
|
||||
Spacer(modifier = Modifier.width(Size16dp))
|
||||
|
||||
RelayStatus(relayViewModel = relayViewModel)
|
||||
RelayStatus(accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -36,6 +37,8 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddBountyAmountViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -80,6 +83,7 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onClose: () -> Unit) {
|
||||
val postViewModel: AddBountyAmountViewModel = viewModel()
|
||||
postViewModel.load(accountViewModel.account, bounty)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@@ -89,7 +93,11 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
)
|
||||
) {
|
||||
Surface() {
|
||||
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -102,8 +110,10 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.hasChanged()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -256,11 +256,10 @@ private fun UserRoomCompose(
|
||||
note.createdAt()
|
||||
}
|
||||
}
|
||||
|
||||
val content by remember(note) {
|
||||
derivedStateOf {
|
||||
mutableStateOf(
|
||||
accountViewModel.decrypt(note)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
|
||||
@@ -69,8 +69,6 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -152,17 +150,13 @@ fun LoadedChatMessageCompose(
|
||||
) {
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
}
|
||||
|
||||
WatchForReports(baseNote, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
|
||||
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
|
||||
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
|
||||
WatchForReports(baseNote, accountViewModel) { newState ->
|
||||
if (state != newState) {
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +172,7 @@ fun LoadedChatMessageCompose(
|
||||
if (it) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
innerQuote,
|
||||
@@ -632,7 +627,7 @@ private fun RenderRegularTextNote(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
if (eventContent != null) {
|
||||
@@ -641,7 +636,7 @@ private fun RenderRegularTextNote(
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
content = eventContent!!,
|
||||
canPreview = canPreview,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -358,6 +361,12 @@ private fun ParseAuthorCommentAndAmount(
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
fun click(content: MutableState<ZapAmountCommentNotification>, nav: (String) -> Unit) {
|
||||
content.value.user?.let {
|
||||
nav(routeFor(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
@@ -366,11 +375,7 @@ private fun RenderState(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.clickable {
|
||||
content.value.user?.let {
|
||||
nav(routeFor(it))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { click(content, nav) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
DisplayAuthorCommentAndAmount(
|
||||
@@ -418,8 +423,19 @@ fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotifi
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
|
||||
Crossfade(authorComment.value, modifier = amountBoxModifier) {
|
||||
it.amount?.let {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.amount != null
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
modifier = amountBoxModifier,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
authorComment.value.amount?.let {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
@@ -453,8 +469,18 @@ fun CrossfadeToDisplayComment(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Crossfade(authorComment.value) {
|
||||
it.comment?.let {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.comment != null
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
authorComment.value.comment?.let {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
|
||||
@@ -31,14 +31,17 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -83,6 +86,7 @@ import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
@@ -117,11 +121,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
@@ -173,6 +179,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
@@ -196,7 +203,6 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -277,11 +283,7 @@ fun CheckHiddenNoteCompose(
|
||||
// Ignores reports as well
|
||||
val state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -324,13 +326,6 @@ fun CheckHiddenNoteCompose(
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class NoteComposeReportState(
|
||||
val isAcceptable: Boolean,
|
||||
val canPreview: Boolean,
|
||||
val relevantReports: ImmutableSet<Note>
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LoadedNoteCompose(
|
||||
note: Note,
|
||||
@@ -347,19 +342,14 @@ fun LoadedNoteCompose(
|
||||
) {
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
|
||||
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
|
||||
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
|
||||
WatchForReports(note, accountViewModel) { newState ->
|
||||
if (state != newState) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
state = newState
|
||||
}
|
||||
@@ -386,7 +376,7 @@ fun LoadedNoteCompose(
|
||||
|
||||
@Composable
|
||||
fun RenderReportState(
|
||||
state: NoteComposeReportState,
|
||||
state: AccountViewModel.NoteComposeReportState,
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -405,6 +395,7 @@ fun RenderReportState(
|
||||
if (showHiddenNote) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
@@ -436,7 +427,7 @@ fun RenderReportState(
|
||||
fun WatchForReports(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChange: (Boolean, Boolean, ImmutableSet<Note>) -> Unit
|
||||
onChange: (AccountViewModel.NoteComposeReportState) -> Unit
|
||||
) {
|
||||
val userFollowsState by accountViewModel.userFollows.observeAsState()
|
||||
val noteReportsState by note.live().reports.observeAsState()
|
||||
@@ -1071,6 +1062,62 @@ private fun NoteBody(
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
|
||||
val noteEvent = baseNote.event
|
||||
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
|
||||
if (zapSplits && noteEvent != null) {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
DisplayZapSplits(noteEvent, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
|
||||
|
||||
if (list.isEmpty()) return
|
||||
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(20.dp)
|
||||
.width(25.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ArrowForwardIos,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
FlowRow {
|
||||
list.forEach {
|
||||
if (it.isLnAddress) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(it.lnAddressOrPubKeyHex),
|
||||
onClick = { },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
)
|
||||
} else {
|
||||
UserPicture(userHex = it.lnAddressOrPubKeyHex, size = 25.dp, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1576,10 +1623,8 @@ private fun RenderPrivateMessage(
|
||||
val noteEvent = note.event as? PrivateDmEvent ?: return
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
|
||||
if (withMe) {
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
@@ -1589,7 +1634,7 @@ private fun RenderPrivateMessage(
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
text = eventContent!!,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -1600,7 +1645,7 @@ private fun RenderPrivateMessage(
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
content = eventContent!!,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
@@ -1610,7 +1655,7 @@ private fun RenderPrivateMessage(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
DisplayUncitedHashtags(hashtags, eventContent!!, nav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1641,9 +1686,9 @@ fun DisplayRelaySet(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? RelaySetEvent ?: return
|
||||
|
||||
val relays by remember {
|
||||
mutableStateOf<ImmutableList<String>>(
|
||||
noteEvent.relays().toImmutableList()
|
||||
val relays by remember(baseNote) {
|
||||
mutableStateOf(
|
||||
noteEvent.relays().map { RelayBriefInfo(it) }.toImmutableList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1698,7 +1743,7 @@ fun DisplayRelaySet(
|
||||
toMembersShow.forEach { relay ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
||||
Text(
|
||||
relay.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/"),
|
||||
text = relay.displayUrl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -1708,7 +1753,7 @@ fun DisplayRelaySet(
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
RelayOptionsAction(relay, accountViewModel, nav)
|
||||
RelayOptionsAction(relay.url, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2414,40 +2459,41 @@ private fun ReplyRow(
|
||||
}
|
||||
}
|
||||
|
||||
val showChannelInfo by remember(note) {
|
||||
derivedStateOf {
|
||||
if (noteEvent is ChannelMessageEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
note.channelHex()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colors.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } }
|
||||
if (replyingDirectlyTo != null && unPackReply) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
} else {
|
||||
// ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
val showChannelReply by remember {
|
||||
derivedStateOf {
|
||||
(noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) ||
|
||||
(noteEvent is LiveActivitiesChatMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()))
|
||||
} else if (showChannelInfo != null) {
|
||||
val replies = remember { note.replyTo?.toImmutableList() }
|
||||
val mentions = remember {
|
||||
(note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList()
|
||||
?: persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
if (showChannelReply) {
|
||||
val channelHex = note.channelHex()
|
||||
channelHex?.let {
|
||||
ChannelHeader(
|
||||
channelHex = channelHex,
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
sendToChannel = true,
|
||||
modifier = remember { Modifier.padding(vertical = 5.dp) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
val replies = remember { note.replyTo?.toImmutableList() }
|
||||
val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() }
|
||||
|
||||
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
|
||||
}
|
||||
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2680,14 +2726,14 @@ private fun BadgeBox(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isRepost by remember {
|
||||
val isRepost by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||
}
|
||||
}
|
||||
|
||||
if (isRepost) {
|
||||
val baseReply by remember {
|
||||
val baseReply by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@@ -35,6 +36,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
@@ -283,6 +286,12 @@ fun ZapVote(
|
||||
}
|
||||
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToPay by remember {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||
persistentListOf()
|
||||
)
|
||||
}
|
||||
|
||||
var zappingProgress by remember { mutableStateOf(0f) }
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
@@ -298,7 +307,7 @@ fun ZapVote(
|
||||
// interactionSource = remember { MutableInteractionSource() },
|
||||
// indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
onClick = {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
@@ -362,6 +371,8 @@ fun ZapVote(
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
},
|
||||
zapType = accountViewModel.account.defaultZapType
|
||||
)
|
||||
} else {
|
||||
@@ -393,10 +404,19 @@ fun ZapVote(
|
||||
scope.launch(Dispatchers.Main) {
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
|
||||
wantsToPay = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||
@@ -463,7 +483,8 @@ fun FilteredZapAmountChoicePopup(
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -502,6 +523,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
context,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
@@ -526,6 +548,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
context,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
|
||||
@@ -78,6 +78,7 @@ import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
@@ -107,6 +108,8 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -543,12 +546,16 @@ fun ReplyReaction(
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,12 +659,22 @@ fun BoostReaction(
|
||||
wantsToBoost = true
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
}
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -675,6 +692,11 @@ fun BoostReaction(
|
||||
onQuote = {
|
||||
wantsToBoost = false
|
||||
onQuotePress()
|
||||
},
|
||||
onRepost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -738,6 +760,20 @@ fun LikeReaction(
|
||||
context,
|
||||
onMultipleChoices = {
|
||||
wantsToReact = true
|
||||
},
|
||||
onWantsToSignReaction = {
|
||||
if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val reaction = accountViewModel.account.reactionChoices.first()
|
||||
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
|
||||
accountViewModel.deleteReactionTo(baseNote, reaction)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote, reaction)
|
||||
}
|
||||
}
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
wantsToReact = true
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -857,7 +893,8 @@ private fun likeClick(
|
||||
accountViewModel: AccountViewModel,
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onMultipleChoices: () -> Unit
|
||||
onMultipleChoices: () -> Unit,
|
||||
onWantsToSignReaction: () -> Unit
|
||||
) {
|
||||
if (accountViewModel.account.reactionChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
@@ -870,14 +907,18 @@ private fun likeClick(
|
||||
.show()
|
||||
}
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onWantsToSignReaction()
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -907,6 +948,11 @@ fun ZapReaction(
|
||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
var wantsToPay by remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||
persistentListOf()
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -932,6 +978,15 @@ fun ZapReaction(
|
||||
},
|
||||
onMultipleChoices = {
|
||||
wantsToZap = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -965,6 +1020,9 @@ fun ZapReaction(
|
||||
scope.launch(Dispatchers.Main) {
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -975,7 +1033,10 @@ fun ZapReaction(
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let {
|
||||
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
@@ -983,11 +1044,38 @@ fun ZapReaction(
|
||||
}
|
||||
|
||||
if (wantsToChangeZapAmount) {
|
||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
|
||||
UpdateZapAmountDialog(
|
||||
onClose = { wantsToChangeZapAmount = false },
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
|
||||
wantsToPay = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsToSetCustomZap) {
|
||||
ZapCustomDialog({ wantsToSetCustomZap = false }, accountViewModel, baseNote)
|
||||
ZapCustomDialog(
|
||||
onClose = { wantsToSetCustomZap = false },
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
wantsToPay = it
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
baseNote = baseNote
|
||||
)
|
||||
}
|
||||
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
@@ -1020,7 +1108,9 @@ private fun zapClick(
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onZappingProgress: (Float) -> Unit,
|
||||
onMultipleChoices: () -> Unit
|
||||
onMultipleChoices: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
@@ -1032,7 +1122,7 @@ private fun zapClick(
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
@@ -1049,20 +1139,14 @@ private fun zapClick(
|
||||
null,
|
||||
"",
|
||||
context,
|
||||
onError = {
|
||||
scope.launch {
|
||||
onZappingProgress(0f)
|
||||
Toast
|
||||
.makeText(context, it, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onZappingProgress(it)
|
||||
}
|
||||
},
|
||||
zapType = accountViewModel.account.defaultZapType
|
||||
zapType = accountViewModel.account.defaultZapType,
|
||||
onPayViaIntent = onPayViaIntent
|
||||
)
|
||||
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
||||
onMultipleChoices()
|
||||
@@ -1178,7 +1262,7 @@ private fun DrawViewCount(
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) {
|
||||
private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit, onRepost: () -> Unit) {
|
||||
val iconSizePx = with(LocalDensity.current) {
|
||||
-iconSize.toPx().toInt()
|
||||
}
|
||||
@@ -1193,8 +1277,13 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
if (accountViewModel.isWriteable()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
onRepost()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
@@ -1373,10 +1462,11 @@ fun ZapAmountChoicePopup(
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val zapMessage = ""
|
||||
@@ -1399,6 +1489,7 @@ fun ZapAmountChoicePopup(
|
||||
context,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
account.defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
@@ -1423,6 +1514,7 @@ fun ZapAmountChoicePopup(
|
||||
context,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
account.defaultZapType
|
||||
)
|
||||
onDismiss()
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -43,11 +43,13 @@ import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -73,9 +75,7 @@ fun NoteAuthorPicture(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
val author by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.distinctUntilChanged().observeAsState(baseNote.author)
|
||||
val author by baseNote.live().authorChanges.observeAsState(baseNote.author)
|
||||
|
||||
Crossfade(targetState = author) {
|
||||
if (it == null) {
|
||||
@@ -104,6 +104,32 @@ fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserPicture(
|
||||
userHex: String,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = remember { Modifier },
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadUser(baseUserHex = userHex) {
|
||||
if (it != null) {
|
||||
UserPicture(
|
||||
user = it,
|
||||
size = size,
|
||||
pictureModifier = pictureModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
DisplayBlankAuthor(
|
||||
size,
|
||||
pictureModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserPicture(
|
||||
user: User,
|
||||
@@ -112,23 +138,19 @@ fun UserPicture(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val route by remember {
|
||||
val route by remember(user) {
|
||||
derivedStateOf {
|
||||
"User/${user.pubkeyHex}"
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ClickableUserPicture(
|
||||
baseUser = user,
|
||||
size = size,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = pictureModifier,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
nav(route)
|
||||
}
|
||||
nav(route)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -274,9 +296,7 @@ fun InnerBaseUserPicture(
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val userProfile by baseUser.live().metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
|
||||
val userProfile by baseUser.live().profilePictureChanges.observeAsState(baseUser.profilePicture())
|
||||
|
||||
PictureAndFollowingMark(
|
||||
userHex = baseUser.pubkeyHex,
|
||||
@@ -391,11 +411,15 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (!state.isFollowingAuthor) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.follow(
|
||||
note.author ?: return@DropdownMenuItem
|
||||
); onDismiss()
|
||||
}) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.follow(author)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.follow))
|
||||
}
|
||||
Divider()
|
||||
@@ -446,36 +470,104 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.quick_action_share))
|
||||
}
|
||||
Divider()
|
||||
if (state.isPrivateBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_private_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_private_bookmarks))
|
||||
}
|
||||
}
|
||||
if (state.isPublicBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_public_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_public_bookmarks))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.broadcast))
|
||||
}
|
||||
Divider()
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.request_deletion))
|
||||
if (state.isPrivateBookmarkNote) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.removePrivateBookmark(note, bookmarks?.decryptedContent ?: "")
|
||||
} else {
|
||||
accountViewModel.removePrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.remove_from_private_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
|
||||
Text(stringResource(R.string.block_report))
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.addPrivateBookmark(note, bookmarks?.decryptedContent ?: "")
|
||||
} else {
|
||||
accountViewModel.addPrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.add_to_private_bookmarks))
|
||||
}
|
||||
}
|
||||
if (state.isPublicBookmarkNote) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.removePublicBookmark(
|
||||
note,
|
||||
bookmarks?.decryptedContent ?: ""
|
||||
)
|
||||
} else {
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.remove_from_public_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.addPublicBookmark(
|
||||
note,
|
||||
bookmarks?.decryptedContent ?: ""
|
||||
)
|
||||
} else {
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.add_to_public_bookmarks))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
@@ -494,6 +586,16 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.content_warning_see_warnings))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.request_deletion))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
|
||||
Text(stringResource(R.string.block_report))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reportDialogShowing) {
|
||||
@@ -508,9 +610,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
|
||||
val followState by accountViewModel.userProfile().live().follows.observeAsState()
|
||||
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
|
||||
val showSensitiveContent by accountViewModel.accountLiveData.map {
|
||||
it.account.showSensitiveContent
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.account.showSensitiveContent)
|
||||
val showSensitiveContent by accountViewModel.showSensitiveContentChanges.observeAsState(accountViewModel.account.showSensitiveContent)
|
||||
|
||||
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
|
||||
launch(Dispatchers.IO) {
|
||||
|
||||
@@ -1,38 +1,62 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.widget.Toast
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ZapOptionstViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -61,17 +85,21 @@ class ZapOptionstViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, baseNote: Note) {
|
||||
fun ZapCustomDialog(
|
||||
onClose: () -> Unit,
|
||||
onError: (text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
baseNote: Note
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val postViewModel: ZapOptionstViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
postViewModel.load(accountViewModel.account)
|
||||
}
|
||||
|
||||
var zappingProgress by remember { mutableStateOf(0f) }
|
||||
|
||||
val zapTypes = listOf(
|
||||
Triple(LnZapEvent.ZapType.PUBLIC, stringResource(id = R.string.zap_type_public), stringResource(id = R.string.zap_type_public_explainer)),
|
||||
Triple(LnZapEvent.ZapType.PRIVATE, stringResource(id = R.string.zap_type_private), stringResource(id = R.string.zap_type_private_explainer)),
|
||||
@@ -111,20 +139,11 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
|
||||
null,
|
||||
postViewModel.customMessage.text,
|
||||
context,
|
||||
onError = {
|
||||
zappingProgress = 0f
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress,
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
zapType = selectedZapType
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,3 +294,115 @@ fun ErrorMessageDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PayViaIntentDialog(
|
||||
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Surface() {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onPress = onClose)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
payingInvoices.forEachIndexed { index, it ->
|
||||
val paid = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
|
||||
if (it.user != null) {
|
||||
BaseUserPicture(it.user, Size55dp, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
DisplayBlankAuthor(size = Size55dp)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (it.user != null) {
|
||||
UsernameDisplay(it.user, showPlayButton = false)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_number, index + 1),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Row() {
|
||||
Text(
|
||||
text = showAmount((it.amountMilliSats / 1000.0f).toBigDecimal()),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Text(
|
||||
text = stringResource(id = R.string.sats),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
PayButton(isActive = !paid.value) {
|
||||
paid.value = true
|
||||
|
||||
val uri = "lightning:" + it.invoice
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PayButton(isActive: Boolean, modifier: Modifier = Modifier, onPost: () -> Unit = {}) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
onPost()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
if (isActive) {
|
||||
Text(text = stringResource(R.string.pay), color = Color.White)
|
||||
} else {
|
||||
Text(text = stringResource(R.string.paid), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ fun UserActionOptions(
|
||||
baseAuthor: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchIsHiddenUser(baseAuthor, accountViewModel) { isHidden ->
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
@@ -209,14 +211,20 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
if (isFollowing) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -227,14 +235,20 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseAuthor)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -45,21 +45,21 @@ class AccountStateViewModel(val context: Context) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun startUI(key: String, useProxy: Boolean, proxyPort: Int) {
|
||||
fun startUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) {
|
||||
val parsed = Nip19.uriToRoute(key)
|
||||
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
|
||||
val account =
|
||||
if (key.startsWith("nsec")) {
|
||||
Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort)
|
||||
Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
|
||||
} else if (pubKeyParsed != null) {
|
||||
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort)
|
||||
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
|
||||
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
||||
// Evaluate NIP-5
|
||||
Account(KeyPair(), proxy = proxy, proxyPort = proxyPort)
|
||||
Account(KeyPair(), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
|
||||
} else {
|
||||
Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort)
|
||||
Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = loginWithExternalSigner)
|
||||
}
|
||||
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -262,6 +262,7 @@ fun NoteMaster(
|
||||
|
||||
HiddenNote(
|
||||
reports,
|
||||
note.author?.let { account.isHidden(it) } ?: false,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
false,
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
@@ -33,16 +33,15 @@ import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
@@ -56,22 +55,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
|
||||
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
|
||||
|
||||
val discoveryListLiveData = accountLiveData.map {
|
||||
val discoveryListLiveData = account.live.map {
|
||||
it.account.defaultDiscoveryFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val homeListLiveData = accountLiveData.map {
|
||||
val homeListLiveData = account.live.map {
|
||||
it.account.defaultHomeFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val notificationListLiveData = accountLiveData.map {
|
||||
val notificationListLiveData = account.live.map {
|
||||
it.account.defaultNotificationFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val storiesListLiveData = accountLiveData.map {
|
||||
val storiesListLiveData = account.live.map {
|
||||
it.account.defaultStoriesFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val showSensitiveContentChanges = account.live.map {
|
||||
it.account.showSensitiveContent
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun updateAutomaticallyStartPlayback(
|
||||
automaticallyStartPlayback: ConnectivityType
|
||||
) {
|
||||
@@ -94,6 +97,10 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return account.isWriteable()
|
||||
}
|
||||
|
||||
fun loggedInWithExternalSigner(): Boolean {
|
||||
return account.loginWithExternalSigner
|
||||
}
|
||||
|
||||
fun userProfile(): User {
|
||||
return account.userProfile()
|
||||
}
|
||||
@@ -205,78 +212,22 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return null
|
||||
}
|
||||
|
||||
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
fun zap(
|
||||
note: Note,
|
||||
amount: Long,
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
zapType: LnZapEvent.ZapType
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
|
||||
ZapPaymentHandler(account).zap(note, amount, pollOption, message, context, onError, onProgress, onPayViaIntent, zapType)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
||||
return
|
||||
}
|
||||
|
||||
var zapRequestJson = ""
|
||||
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType)
|
||||
if (zapRequest != null) {
|
||||
zapRequestJson = zapRequest.toJson()
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(0.10f)
|
||||
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount,
|
||||
message,
|
||||
zapRequestJson,
|
||||
onSuccess = {
|
||||
onProgress(0.7f)
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = it,
|
||||
note,
|
||||
onResponse = { response ->
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
)
|
||||
onProgress(0.8f)
|
||||
|
||||
// Awaits for the event to come back to LocalCache.
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(5000)
|
||||
onProgress(0f)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
onError(context.getString(R.string.lightning_wallets_not_found))
|
||||
}
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
)
|
||||
}
|
||||
|
||||
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
|
||||
account.report(note, type, content)
|
||||
}
|
||||
@@ -304,10 +255,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
account.addPrivateBookmark(note)
|
||||
}
|
||||
|
||||
fun addPrivateBookmark(note: Note, decryptedContent: String) {
|
||||
account.addPrivateBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun addPublicBookmark(note: Note, decryptedContent: String) {
|
||||
account.addPublicBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun removePublicBookmark(note: Note, decryptedContent: String) {
|
||||
account.removePublicBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun addPublicBookmark(note: Note) {
|
||||
account.addPublicBookmark(note)
|
||||
}
|
||||
|
||||
fun removePrivateBookmark(note: Note, decryptedContent: String) {
|
||||
account.removePrivateBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun removePrivateBookmark(note: Note) {
|
||||
account.removePrivateBookmark(note)
|
||||
}
|
||||
@@ -410,14 +377,24 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return account.defaultZapType
|
||||
}
|
||||
|
||||
fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet<Note>) -> Unit) {
|
||||
@Immutable
|
||||
data class NoteComposeReportState(
|
||||
val isAcceptable: Boolean = true,
|
||||
val canPreview: Boolean = true,
|
||||
val isHiddenAuthor: Boolean = false,
|
||||
val relevantReports: ImmutableSet<Note> = persistentSetOf()
|
||||
)
|
||||
|
||||
fun isNoteAcceptable(note: Note, onReady: (NoteComposeReportState) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
|
||||
if (isFromLoggedIn || isFromLoggedInFollow) {
|
||||
// No need to process if from trusted people
|
||||
onReady(true, true, persistentSetOf())
|
||||
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
|
||||
} else if (note.author?.let { account.isHidden(it) } == true) {
|
||||
onReady(NoteComposeReportState(false, false, true, persistentSetOf()))
|
||||
} else {
|
||||
val newCanPreview = !note.hasAnyReports()
|
||||
|
||||
@@ -425,11 +402,18 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
|
||||
if (newCanPreview && newIsAcceptable) {
|
||||
// No need to process reports if nothing is wrong
|
||||
onReady(true, true, persistentSetOf())
|
||||
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
|
||||
} else {
|
||||
val newRelevantReports = account.getRelevantReports(note)
|
||||
|
||||
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
|
||||
onReady(
|
||||
NoteComposeReportState(
|
||||
newIsAcceptable,
|
||||
newCanPreview,
|
||||
false,
|
||||
newRelevantReports.toImmutableSet()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,6 +513,16 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveRelayDocument(
|
||||
dirtyUrl: String,
|
||||
onInfo: (RelayInformation) -> Unit,
|
||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Nip11CachedRetriever.loadRelayInfo(dirtyUrl, onInfo, onError)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||
return AccountViewModel(account) as AccountViewModel
|
||||
|
||||
@@ -171,14 +171,20 @@ fun GeoHashActionOptions(
|
||||
if (isFollowingTag) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowGeohash(tag)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -189,14 +195,20 @@ fun GeoHashActionOptions(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followGeohash(tag)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -147,14 +147,20 @@ fun HashtagActionOptions(
|
||||
if (isFollowingTag) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowHashtag(tag)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -165,14 +171,20 @@ fun HashtagActionOptions(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followHashtag(tag)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -9,15 +9,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.DrawerValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.rememberDrawerState
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
@@ -28,6 +19,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
@@ -37,12 +29,6 @@ import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.*
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppTopBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountState
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
@@ -251,11 +237,14 @@ fun FloatingButtons(
|
||||
navScrollToTop: (Route, Boolean) -> Unit
|
||||
) {
|
||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
when (state) {
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
// Does nothing.
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
|
||||
}
|
||||
}
|
||||
is AccountState.LoggedOff -> {
|
||||
// Does nothing.
|
||||
|
||||
@@ -621,7 +621,9 @@ private fun ProfileHeader(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.Center),
|
||||
onClick = { popupExpanded = true },
|
||||
onClick = {
|
||||
popupExpanded = true
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
@@ -714,6 +716,8 @@ private fun ProfileActions(
|
||||
EditButton(accountViewModel.account)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchIsHiddenUser(baseUser, accountViewModel) { isHidden ->
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
@@ -744,14 +748,20 @@ private fun DisplayFollowUnfollowButton(
|
||||
if (isLoggedInFollowingUser) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollow(baseUser)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -763,14 +773,20 @@ private fun DisplayFollowUnfollowButton(
|
||||
if (isUserFollowingLoggedIn) {
|
||||
FollowButton(R.string.follow_back) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -781,14 +797,20 @@ private fun DisplayFollowUnfollowButton(
|
||||
} else {
|
||||
FollowButton(R.string.follow) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -1688,17 +1710,21 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
if (accountViewModel.userProfile() != user) {
|
||||
Divider()
|
||||
if (accountViewModel.account.isHidden(user)) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.show(user)
|
||||
onDismiss()
|
||||
}) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
accountViewModel.show(user)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.unblock_user))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.hide(user)
|
||||
onDismiss()
|
||||
}) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
accountViewModel.hide(user)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.block_hide_user))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
|
||||
|
||||
val feedViewModel: NostrThreadFeedViewModel = viewModel(
|
||||
key = noteId + "NostrThreadFeedViewModel",
|
||||
factory = NostrThreadFeedViewModel.Factory(noteId)
|
||||
factory = NostrThreadFeedViewModel.Factory(accountViewModel.account, noteId)
|
||||
)
|
||||
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteComposeReportState
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRelay
|
||||
@@ -76,7 +76,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -128,7 +127,6 @@ fun WatchAccountForVideoScreen(videoFeedView: NostrVideoFeedViewModel, accountVi
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SaveableFeedState(
|
||||
videoFeedView: NostrVideoFeedViewModel,
|
||||
@@ -250,19 +248,14 @@ fun LoadedVideoCompose(
|
||||
) {
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
AccountViewModel.NoteComposeReportState()
|
||||
)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
|
||||
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
|
||||
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
|
||||
WatchForReports(note, accountViewModel) { newState ->
|
||||
if (state != newState) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
state = newState
|
||||
}
|
||||
@@ -281,7 +274,7 @@ fun LoadedVideoCompose(
|
||||
|
||||
@Composable
|
||||
fun RenderReportState(
|
||||
state: NoteComposeReportState,
|
||||
state: AccountViewModel.NoteComposeReportState,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -290,11 +283,12 @@ fun RenderReportState(
|
||||
|
||||
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
|
||||
if (showHiddenNote) {
|
||||
Column(remember { Modifier.fillMaxSize(1f) }, verticalArrangement = Arrangement.Center) {
|
||||
Column(remember { Modifier.fillMaxSize() }, verticalArrangement = Arrangement.Center) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
Modifier.fillMaxWidth(),
|
||||
false,
|
||||
nav,
|
||||
onClick = { showReportedNote = true }
|
||||
@@ -421,14 +415,11 @@ private fun VideoUserOptionAction(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
val noteRelays = remember(noteRelaysState) {
|
||||
noteRelaysState?.note?.relays ?: emptySet()
|
||||
}
|
||||
val noteRelays by baseNote.live().relayInfo.observeAsState()
|
||||
|
||||
FlowRow() {
|
||||
noteRelays.forEach { dirtyUrl ->
|
||||
RenderRelay(dirtyUrl, accountViewModel, nav)
|
||||
noteRelays?.forEach { relayInfo ->
|
||||
RenderRelay(relayInfo, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -36,13 +39,20 @@ import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
import com.vitorpamplona.amethyst.service.SignerType
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@@ -63,6 +73,58 @@ fun LoginPage(
|
||||
val useProxy = remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf("9050") }
|
||||
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||
val activity = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
loginWithExternalSigner = false
|
||||
ExternalSignerUtils.isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
"Sign request rejected",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@rememberLauncherForActivityResult
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
key.value = TextFieldValue(event)
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt(), true)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(loginWithExternalSigner) {
|
||||
if (loginWithExternalSigner) {
|
||||
ExternalSignerUtils.openSigner(
|
||||
"",
|
||||
SignerType.GET_PUBLIC_KEY,
|
||||
activity,
|
||||
"",
|
||||
""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -229,7 +291,7 @@ fun LoginPage(
|
||||
}
|
||||
}
|
||||
|
||||
if (isPackageInstalled(context, "org.torproject.android")) {
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
@@ -290,6 +352,53 @@ fun LoginPage(
|
||||
Text(text = stringResource(R.string.login))
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageUtils.isAmberInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login"), "")
|
||||
if (result == null) {
|
||||
loginWithExternalSigner = true
|
||||
return@Button
|
||||
}
|
||||
key.value = TextFieldValue(result)
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.startUI(
|
||||
key.value.text,
|
||||
useProxy.value,
|
||||
proxyPort.value.toInt(),
|
||||
true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.login_with_external_signer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The last child is glued to the bottom.
|
||||
@@ -315,7 +424,3 @@ fun LoginPage(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun isPackageInstalled(context: Context, target: String): Boolean {
|
||||
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">"<استحالة فك تشفير الرسالة الخاصة>
|
||||
\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">(0–100)%</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>
|
||||
|
||||
499
app/src/main/res/values-bn-rBD/strings.xml
Normal file
499
app/src/main/res/values-bn-rBD/strings.xml
Normal file
@@ -0,0 +1,499 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">QR কোডের দিকে নির্দেশ করুন</string>
|
||||
<string name="show_qr">QR দেখান</string>
|
||||
<string name="profile_image">প্রোফাইল ছবি</string>
|
||||
<string name="scan_qr">QR স্ক্যান করুন</string>
|
||||
<string name="show_anyway">যাহোক, দেখান</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">পোস্টটি কারো দ্বারা অভিযুক্ত হয়েছে</string>
|
||||
<string name="post_not_found">ইভেন্টটি লোড হচ্ছে অথবা আপনার রিলে তালিকায় এটি পাওয়া যাচ্ছে না</string>
|
||||
<string name="channel_image">চ্যানেল ইমেজ</string>
|
||||
<string name="referenced_event_not_found">উল্লেখিত ইভেন্টটি পাওয়া যায় নি</string>
|
||||
<string name="could_not_decrypt_the_message">মেসেজটি ডিক্রিপ্ট করা যায় নি</string>
|
||||
<string name="group_picture">দলগত ছবি</string>
|
||||
<string name="explicit_content">খোলামেলা আধেয়</string>
|
||||
<string name="spam">স্প্যাম</string>
|
||||
<string name="impersonation">ছদ্মবেশ</string>
|
||||
<string name="illegal_behavior">বেআইনী আচরণ</string>
|
||||
<string name="unknown">অচেনা</string>
|
||||
<string name="relay_icon">রিলে আইকন</string>
|
||||
<string name="unknown_author">অজ্ঞাত-পরিচয় লেখক</string>
|
||||
<string name="copy_text">টেক্সটটি কপি করুন</string>
|
||||
<string name="copy_user_pubkey">লেখক আইডি কপি করুন</string>
|
||||
<string name="copy_note_id">নোটের আইডি কপি করুন</string>
|
||||
<string name="broadcast">সম্প্রচার</string>
|
||||
<string name="request_deletion">নোটটি মুছে ফেলার অনুরোধ করুন</string>
|
||||
<string name="block_report">ব্লক/অভিযুক্ত করুন</string>
|
||||
<string name="block_hide_user"><![CDATA[ব্লক করুন এবং ব্যবহারকারীকে আড়ালে রাখুন]]></string>
|
||||
<string name="report_spam_scam">স্প্যাম/প্রতারণার অভিযোগ করুন</string>
|
||||
<string name="report_impersonation">ছদ্মবেশ ধারণের অভিযোগ করুন</string>
|
||||
<string name="report_explicit_content">খোলামেলা আধেয় হিসেবে অভিযোগ করুন</string>
|
||||
<string name="report_illegal_behaviour">বেআইনী আচরণের অভিযোগ করুন</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">প্রতিউত্তর দিতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">পোস্টগুলি বুস্ট করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">পোস্টগুলি লাইক করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">জ্যাপের কোনো পরিমাণ নির্দিষ্ট করা নেই। এটি পরিবর্তন করতে চাইলে দীর্ঘক্ষণ চাপুন</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">কাউকে জ্যাপ পাঠাতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">কাউকে অনুসরণ করতে ব্যক্তিগত চাবি দিয়ে লগইন করুন</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">কাউকে আনফলো করতে ব্যক্তিগত চাবি দিয়ে লগ ইন করুন</string>
|
||||
<string name="zaps">জ্যাপস</string>
|
||||
<string name="view_count">পরিমাণ দেখুন</string>
|
||||
<string name="boost">বুস্ট</string>
|
||||
<string name="boosted">বুস্ট করা হয়েছে</string>
|
||||
<string name="quote">উদ্ধৃতি দিন</string>
|
||||
<string name="new_amount_in_sats">স্যাটে নতুন পরিমাণ যুক্ত হয়েছে</string>
|
||||
<string name="add">যুক্ত করুন</string>
|
||||
<string name="replying_to">"এই ব্যক্তির প্রতিউত্তর দিচ্ছেন "</string>
|
||||
<string name="and">" এবং "</string>
|
||||
<string name="in_channel">"চ্যানেলের ভেতর "</string>
|
||||
<string name="profile_banner">প্রোফাইলের ব্যানার</string>
|
||||
<string name="payment_successful">সফলভাবে মূল্য পরিশোধিত হয়েছে</string>
|
||||
<string name="error_parsing_error_message">এরর পার্সিং এরর মেসেজ</string>
|
||||
<string name="following">" অনুসরণ"</string>
|
||||
<string name="followers">" অনুসারী"</string>
|
||||
<string name="profile">প্রোফাইল</string>
|
||||
<string name="security_filters">নিরাপত্তা-ফিল্টার</string>
|
||||
<string name="log_out">লগ আউট</string>
|
||||
<string name="show_more">আরও দেখান</string>
|
||||
<string name="lightning_invoice">লাইটনিং ইনভয়েস</string>
|
||||
<string name="pay">পরিশোধ করুন</string>
|
||||
<string name="lightning_tips">লাইটনিং টিপস</string>
|
||||
<string name="note_to_receiver">প্রাপকের উদ্দেশে চিরকুট</string>
|
||||
<string name="thank_you_so_much">আপনাকে অনেক ধন্যবাদ!</string>
|
||||
<string name="amount_in_sats">স্যাটে মোট পরিমাণ</string>
|
||||
<string name="send_sats">স্যাট পাঠান</string>
|
||||
<string name="error_parsing_preview_for">"এই দুইয়ের পূর্বরূপ দেখানো যাচ্ছে না %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"%1$s এর জন্য কার্ড ইমেজের পূর্বরূপ দেখুন"</string>
|
||||
<string name="new_channel">নতুন চ্যানেল</string>
|
||||
<string name="channel_name">চ্যানেলের নাম</string>
|
||||
<string name="my_awesome_group">আমার চমৎকার গ্রুপ</string>
|
||||
<string name="picture_url">ছবির Url</string>
|
||||
<string name="description">বিবরণ</string>
|
||||
<string name="about_us">"আমাদের সম্পর্কে.. "</string>
|
||||
<string name="what_s_on_your_mind">নিজের কোনো ভাবনা প্রকাশ করতে চান?</string>
|
||||
<string name="post">পেশ করুন</string>
|
||||
<string name="save">সংরক্ষণ করুন</string>
|
||||
<string name="create">তৈরি করুন</string>
|
||||
<string name="cancel">বাতিল করুন</string>
|
||||
<string name="failed_to_upload_the_image">ছবিটি সফলভাবে আপলোড করা যায় নি</string>
|
||||
<string name="relay_address">রিলে ঠিকানা</string>
|
||||
<string name="posts">পোস্টগুলি</string>
|
||||
<string name="bytes">বাইটস</string>
|
||||
<string name="errors">ত্রুটিসমূহ</string>
|
||||
<string name="home_feed">নীড়পাতা</string>
|
||||
<string name="private_message_feed">ব্যক্তিগত বার্তাসমূহ</string>
|
||||
<string name="public_chat_feed">সার্বজনীন কথোপকথন</string>
|
||||
<string name="global_feed">বৈশ্বিক পাতা</string>
|
||||
<string name="search_feed">অনুসন্ধান পাতা</string>
|
||||
<string name="add_a_relay">একটি রিলে যুক্ত করুন</string>
|
||||
<string name="display_name">প্রদর্শিত নাম</string>
|
||||
<string name="my_display_name">আমার প্রদর্শিত নাম</string>
|
||||
<string name="username">ইউজারনেম</string>
|
||||
<string name="my_username">আমার ইউজারনেম</string>
|
||||
<string name="about_me">আমার সম্পর্কে</string>
|
||||
<string name="avatar_url">অবতারের URL</string>
|
||||
<string name="banner_url">ব্যানারের URL</string>
|
||||
<string name="website_url">ওয়েবসাইটের URL</string>
|
||||
<string name="ln_address">LN ঠিকানা</string>
|
||||
<string name="ln_url_outdated">LN URL (অপ্রচলিত)</string>
|
||||
<string name="image_saved_to_the_gallery">ছবিটি গ্যালারিতে সংরক্ষণ করা হয়েছে</string>
|
||||
<string name="failed_to_save_the_image">ছবিটি সফলভাবে সংরক্ষণ করা যায় নি</string>
|
||||
<string name="upload_image">ছবিটি আপলোড করুন</string>
|
||||
<string name="uploading">আপলোড করা হচ্ছে…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">স্যাট গ্রহণের জন্য ব্যবহারকারীর কোনো লাইটনিং ঠিকানা দেওয়া নেই</string>
|
||||
<string name="reply_here">"এখানে প্রতিউত্তর দিন.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">নস্টারে শেয়ারের জন্য নোট আইডিটি ক্লিপবোর্ডে কপি করা হয়</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">চ্যানেল আইডি (নোট) ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="edits_the_channel_metadata">চ্যানেলের মেটাডেটা সম্পাদনা করে</string>
|
||||
<string name="join">যুক্ত হোন</string>
|
||||
<string name="known">জ্ঞাত</string>
|
||||
<string name="new_requests">নয়া অনুরোধসমূহ</string>
|
||||
<string name="blocked_users">ব্লক করে রাখা ব্যবহারকারীরা</string>
|
||||
<string name="new_threads">নতুন থ্রেড</string>
|
||||
<string name="conversations">কথোপকথন</string>
|
||||
<string name="notes">নোটস</string>
|
||||
<string name="replies">প্রতিউত্তরসমূহ</string>
|
||||
<string name="follows">"অনুসারী"</string>
|
||||
<string name="reports">"অভিযোগসমূহ"</string>
|
||||
<string name="more_options">আরও বিকল্প</string>
|
||||
<string name="relays">" রিলেগুলি"</string>
|
||||
<string name="website">ওয়েবসাইট</string>
|
||||
<string name="lightning_address">লাইটনিং ঠিকানা</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">ব্যাকআপের জন্য আপনার Nsec আইডি (তথা পাসওয়ার্ড) ক্লিপবোর্ডে কপি করে</string>
|
||||
<string name="copy_private_key_to_the_clipboard">আপনার ব্যক্তিগত চাবিটি ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">অন্যদের সাথে শেয়ারের জন্য আপনার গণ চাবি ক্লিপবোর্ডে কপি করে</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">গণ চাবি (Npub) ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="send_a_direct_message">একটি সরাসরি-বার্তা পাঠান</string>
|
||||
<string name="edits_the_user_s_metadata">ব্যবহারকারীর মেটাডেটা সম্পাদনা করে</string>
|
||||
<string name="follow">অনুসরণ করুন</string>
|
||||
<string name="follow_back">প্রতি-অনুসরণ করুন</string>
|
||||
<string name="unblock">আনব্লক করুন</string>
|
||||
<string name="copy_user_id">ব্যবহারকারীর আইডি কপি করুন</string>
|
||||
<string name="unblock_user">ব্যবহারকারীকে আনব্লক করুন</string>
|
||||
<string name="npub_hex_username">"Npub, ইউজারনেম, টেক্সট"</string>
|
||||
<string name="clear">মুছে ফেলুন</string>
|
||||
<string name="app_logo">অ্যাপের লোগো</string>
|
||||
<string name="nsec_npub_hex_private_key">Nsec.. অথবা Npub..</string>
|
||||
<string name="show_password">পাসওয়ার্ড দেখান</string>
|
||||
<string name="hide_password">পাসওয়ার্ড লুকান</string>
|
||||
<string name="invalid_key">অকেজো কি</string>
|
||||
<string name="i_accept_the">"আমি এটা মেনে নিচ্ছি "</string>
|
||||
<string name="terms_of_use">ব্যবহারের শর্তাবলী</string>
|
||||
<string name="acceptance_of_terms_is_required">প্রদত্ত শর্তাবলী কবুল করা আবশ্যক</string>
|
||||
<string name="key_is_required">কি প্রয়োজন</string>
|
||||
<string name="login">লগ ইন</string>
|
||||
<string name="generate_a_new_key">একটি নতুন কি তৈরি করুন</string>
|
||||
<string name="loading_feed">ফিডটি লোড করা হচ্ছে</string>
|
||||
<string name="error_loading_replies">"প্রতিউত্তরগুলি লোড করা যাচ্ছে না: "</string>
|
||||
<string name="try_again">আবার চেষ্টা করুন</string>
|
||||
<string name="feed_is_empty">ফিডটি খালি.</string>
|
||||
<string name="refresh">রিফ্রেশ করুন</string>
|
||||
<string name="created">তৈরি করা হয়েছে</string>
|
||||
<string name="with_description_of">এর বিবরণ সহ</string>
|
||||
<string name="and_picture">এবং ছবি</string>
|
||||
<string name="changed_chat_name_to">চ্যাটের নাম পরিবর্তন করুন</string>
|
||||
<string name="description_to">বিবরণ</string>
|
||||
<string name="and_picture_to">এবং ছবি</string>
|
||||
<string name="leave">ছেড়ে দিন</string>
|
||||
<string name="unfollow">আনফলো করুন</string>
|
||||
<string name="channel_created">চ্যানেলটি তৈরি করা হয়েছে</string>
|
||||
<string name="channel_information_changed_to">"চ্যানেলের তথ্যের পরিবর্তিত চেহারা"</string>
|
||||
<string name="public_chat">সার্বজনীন আলাপ</string>
|
||||
<string name="posts_received">পেশকৃত আধেয় গৃহীত হয়েছে</string>
|
||||
<string name="remove">সরিয়ে ফেলুন</string>
|
||||
<string name="translations_auto">স্বয়ংক্রিয়</string>
|
||||
<string name="translations_translated_from">এই মূলপাঠ থেকে অনূদিত হয়েছে</string>
|
||||
<string name="translations_to">থেকে</string>
|
||||
<string name="translations_show_in_lang_first">প্রথমে %1$s ভাষায় দেখান</string>
|
||||
<string name="translations_always_translate_to_lang">সর্বদা %1$s ভাষায় অনুবাদ করুন</string>
|
||||
<string name="translations_never_translate_from_lang">কখনোই %1$s ভাষা থেকে অনুবাদ করবেন না</string>
|
||||
<string name="nip_05">নস্টার ঠিকানা</string>
|
||||
<string name="never">কখনো না</string>
|
||||
<string name="now">এখন</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">নগ্নতা</string>
|
||||
<string name="profanity_hateful_speech">অশ্লীলতা / বিদ্বেষপূর্ণ বক্তব্য</string>
|
||||
<string name="report_hateful_speech">বিদ্বেষপূর্ণ বক্তব্য হিসেবে অভিযোগ জানান</string>
|
||||
<string name="report_nudity_porn">নগ্নতা / পর্ন হিসেবে অভিযোগ জানান</string>
|
||||
<string name="others">অন্যান্য</string>
|
||||
<string name="mark_all_known_as_read">পরিচিত সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
|
||||
<string name="mark_all_new_as_read">নতুন সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
|
||||
<string name="mark_all_as_read">সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string>
|
||||
<string name="backup_keys">ব্যাকআপ চাবিগুলি</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## চাবির ব্যাকআপ এবং নিরাপত্তা বিষয়ক টিপস
|
||||
\n\nআপনার অ্যাকাউন্ট একটি ব্যক্তিগত চাবি দ্বারা সুরক্ষিত। চাবি হলো এলোমেলো অক্ষর ও সংখ্যার একটি দীর্ঘ তন্তু যা **nsec** দ্বারা শুরু হয়। আপনার ব্যক্তিগত চাবিটি পেলে যে কেউ আপনার পরিচয় ব্যবহার করে যেকোনো আধেয় প্রকাশ করতে পারবে।
|
||||
\n\n- আপনি বিশ্বাস করেন না এমন কোনো ওয়েবসাইট কিংবা সফটওয়্যারে আপনার ব্যক্তিগত চাবিটি রাখবেন **না**।
|
||||
\n- অ্যামেথিস্টের ডেভেলপাররা **কখনোই** আপনার কাছে আপনার ব্যক্তিগত চাবিটি জানতে চাইবে না।
|
||||
\n- আপনার ব্যক্তিগত চাবির একটি নিরাপদ ব্যাকআপ **অবশ্যই** সংরক্ষণ করবেন কারণ এটি আপনার অ্যাকাউন্ট পুনরুদ্ধারে কাজে আসবে। আমরা একটি পাসওয়ার্ড ম্যানেজার ব্যবহারের পরামর্শ দিই।
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">ব্যক্তিগত চাবি (nsec) ক্লিপবোর্ডে কপি করা হয়েছে</string>
|
||||
<string name="copy_my_secret_key">আমার ব্যক্তিগত চাবিটি কপি করুন</string>
|
||||
<string name="biometric_authentication_failed">প্রমাণীকরণ ব্যর্থ হয়েছে।</string>
|
||||
<string name="biometric_error">ত্রুটি</string>
|
||||
<string name="badge_created_by">"%1$s দ্বারা নির্মিত"</string>
|
||||
<string name="badge_award_image_for">"%1$s -এর জন্য ব্যাজ পুরষ্কারের ছবি"</string>
|
||||
<string name="new_badge_award_notif">আপনি একটি নতুন ব্যাজ পুরষ্কার পেয়েছেন</string>
|
||||
<string name="award_granted_to">ব্যাজ পুরস্কারটি পাচ্ছে</string>
|
||||
<string name="copied_note_text_to_clipboard">নোটের রচনাটি ক্লিপবোর্ডে কপি করা হয়েছে</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">রচয়িতার @npub ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">নোট আইডিটি (@note1) ক্লিপবোর্ডে কপি করা হয়েছে</string>
|
||||
<string name="select_text_dialog_top">রচনাটি নির্বাচন করুন</string>
|
||||
<string name="private_conversation_notification">"<Unable to decrypt private message>\n\nআপনাকে %1$s এবং %2$s এর মধ্যে একটি ব্যক্তিগত/এনক্রিপ্টেড কথোপকথনে উদ্ধৃত করা হয়েছে৷"</string>
|
||||
<string name="account_switch_add_account_dialog_title">নতুন অ্যাকাউন্ট যুক্ত করুন</string>
|
||||
<string name="drawer_accounts">অ্যাকাউন্টগুলি</string>
|
||||
<string name="account_switch_select_account">অ্যাকাউন্ট নির্বাচন করুন</string>
|
||||
<string name="account_switch_add_account_btn">নতুন অ্যাকাউন্ট যুক্ত করুন</string>
|
||||
<string name="account_switch_active_account">অ্যাকাউন্ট সক্রিয় করুন</string>
|
||||
<string name="account_switch_has_private_key">ব্যক্তিগত চাবি আছে</string>
|
||||
<string name="account_switch_pubkey_only">শুধুমাত্র পাঠযোগ্য, ব্যক্তিগত চাবি নেই</string>
|
||||
<string name="back">পিছে যান</string>
|
||||
<string name="quick_action_select">নির্বাচন করুন</string>
|
||||
<string name="quick_action_share_browser_link">ব্রাউজারের লিংক শেয়ার করুন</string>
|
||||
<string name="quick_action_share">শেয়ার</string>
|
||||
<string name="quick_action_copy_user_id">লেখকের আইডি</string>
|
||||
<string name="quick_action_copy_note_id">নোটের আইডি</string>
|
||||
<string name="quick_action_copy_text">টেক্সটটি কপি করুন</string>
|
||||
<string name="quick_action_delete">মুছে ফেলুন</string>
|
||||
<string name="quick_action_unfollow">আনফলো করুন</string>
|
||||
<string name="quick_action_follow">অনুসরণ করুন</string>
|
||||
<string name="quick_action_request_deletion_alert_title">নোটটি মুছে ফেলার অনুরোধ করুন</string>
|
||||
<string name="quick_action_request_deletion_alert_body">অ্যামেথিস্ট অনুরোধ করবে যেন আপনার নোটটি আপনার বর্তমানে সংযুক্ত রিলেগুলো থেকে মুছে ফেলা হয়। তবে কোনো নিশ্চয়তা নেই যে নোটটি স্থায়ীভাবে সেই রিলেগুলি থেকে বা অন্যান্য রিলে থেকে মুছে ফেলা হবে, যেখানে এটি সংরক্ষণ করা হতে পারে৷</string>
|
||||
<string name="quick_action_block_dialog_btn">ব্লক করুন</string>
|
||||
<string name="quick_action_delete_dialog_btn">মুছে ফেলুন</string>
|
||||
<string name="quick_action_block">ব্লক করুন</string>
|
||||
<string name="quick_action_report">অভিযোগ করুন</string>
|
||||
<string name="quick_action_delete_button">মুছে ফেলুন</string>
|
||||
<string name="quick_action_dont_show_again_button">আর দেখাবেন না</string>
|
||||
<string name="report_dialog_spam">স্প্যাম অথবা প্রতারণা</string>
|
||||
<string name="report_dialog_profanity">অশ্লীলতা বা বিদ্বেষপূর্ণ আচরণ</string>
|
||||
<string name="report_dialog_impersonation">ক্রূর উদ্দেশ্য সাধনে ছদ্মবেশ</string>
|
||||
<string name="report_dialog_nudity">নগ্নতা বা গ্রাফিক আধেয়</string>
|
||||
<string name="report_dialog_illegal">বেআইনী আচরণ</string>
|
||||
<string name="report_dialog_blocking_a_user">কোনো ব্যবহারকারীকে ব্লক করে রাখলে আপনার অ্যাপে তাদের কোনো আধেয় দেখা যাবে না। তবে আপনার নোটগুলি সর্বজনীনভাবে উন্মুক্ত, আপনার দ্বারা ব্লক করা ব্যক্তিরাও তা দেখতে পাবে। ব্লক করে রাখা ব্যবহারকারীদেরকে নিরাপত্তা-ফিল্টার স্ক্রিনে তালিকাভুক্ত করা হয়েছে।</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[ব্যবহারকারীকে ব্লক করুন এবং আড়ালে রাখুন]]></string>
|
||||
<string name="report_dialog_report_btn">অবমাননার অভিযোগ করুন</string>
|
||||
<string name="report_dialog_reminder_public">পেশকৃত সমস্ত অভিযোগ সর্বজনীনভাবে দৃশ্যমান হবে৷</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">আপনি চাইলে আপনার অভিযোগের সাথে অতিরিক্ত অনুসঙ্গ যোগ করতে পারেন…</string>
|
||||
<string name="report_dialog_additional_reason_label">সংযোজিত অনুসঙ্গ</string>
|
||||
<string name="report_dialog_select_reason_label">কারণ</string>
|
||||
<string name="report_dialog_select_reason_placeholder">একটি কারণ নির্বাচন করুন…</string>
|
||||
<string name="report_dialog_post_report_btn">অভিযোগ পেশ করুন</string>
|
||||
<string name="report_dialog_title">ব্লক করুন এবং অভিযোগ পেশ করুন</string>
|
||||
<string name="block_only">ব্লক করুন</string>
|
||||
<string name="bookmarks">বুকমার্ক করুন</string>
|
||||
<string name="private_bookmarks">ব্যক্তিগত বুকমার্ক</string>
|
||||
<string name="public_bookmarks">সর্বজনীন বুকমার্ক</string>
|
||||
<string name="add_to_private_bookmarks">ব্যক্তিগত বুকমার্কে যুক্ত করুন</string>
|
||||
<string name="add_to_public_bookmarks">সর্বজনীন বুকমার্কে যুক্ত করুন</string>
|
||||
<string name="remove_from_private_bookmarks">ব্যক্তিগত বুকমার্ক থেকে সরিয়ে দিন</string>
|
||||
<string name="remove_from_public_bookmarks">সর্বজনীন বুকমার্ক থেকে সরিয়ে দিন</string>
|
||||
<string name="wallet_connect_service">ওয়ালেট সংযোগের পরিষেবা</string>
|
||||
<string name="wallet_connect_service_explainer">অ্যাপ থেকে বের না হয়েই জ্যাপ প্রদান করতে একটি নস্টার-সিক্রেট অনুমোদন করা হয়ে থাকে। এই নস্টার-সিক্রেটটি নিরাপদে রাখুন এবং সম্ভব হলে একটি প্রাইভেট রিলে ব্যবহার করুন</string>
|
||||
<string name="wallet_connect_service_pubkey">ওয়ালেট সংযোগের গণ চাবি</string>
|
||||
<string name="wallet_connect_service_relay">ওয়ালেট সংযোগের রিলে</string>
|
||||
<string name="wallet_connect_service_secret">ওয়ালেট সংযোগের সিক্রেট</string>
|
||||
<string name="wallet_connect_service_show_secret">গোপন চাবি দেখান</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">nsec / hex ব্যক্তিগত চাবি</string>
|
||||
<string name="pledge_amount_in_sats">প্রতিশ্রুত পরিমাণ স্যাট দ্বারা জানান</string>
|
||||
<string name="post_poll">একটি পোল পেশ করুন</string>
|
||||
<string name="poll_heading_required">প্রয়োজনীয় ক্ষেত্রসমূহ:</string>
|
||||
<string name="poll_zap_recipients">জ্যাপ প্রাপকেরা</string>
|
||||
<string name="poll_primary_description">পোলের প্রাথমিক বিবরণ…</string>
|
||||
<string name="poll_option_index">বিকল্প %s</string>
|
||||
<string name="poll_option_description">পোলের বিবিধ বিকল্পের বিবরণ</string>
|
||||
<string name="poll_heading_optional">ঐচ্ছিক ক্ষেত্র:</string>
|
||||
<string name="poll_zap_value_min">ন্যূনতম জ্যাপ</string>
|
||||
<string name="poll_zap_value_max">সর্বোচ্চ জ্যাপ</string>
|
||||
<string name="poll_consensus_threshold">ঐক্যমত</string>
|
||||
<string name="poll_consensus_threshold_percent">(০–১০০)%</string>
|
||||
<string name="poll_closing_time">এই সময় পর বন্ধ করুন</string>
|
||||
<string name="poll_closing_time_days">দিনগুলি</string>
|
||||
<string name="poll_is_closed">পোলটি আর নতুন ভোট গ্রহণ করবে না</string>
|
||||
<string name="poll_zap_amount">পরিমাণটি জ্যাপ করুন</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">এই জাতীয় পোলে একজন ব্যবহারকারী শুধুমাত্র একটি করেই ভোট দিতে পারবেন</string>
|
||||
<string name="looking_for_event">"ইভেন্ট খোঁজা হচ্ছে %1$s"</string>
|
||||
<string name="custom_zaps_add_a_message">একটি সর্বজনীন বার্তা পাঠান</string>
|
||||
<string name="custom_zaps_add_a_message_private">একটি ব্যক্তিগত বার্তা পাঠান</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">একটি ইনভয়েস বার্তা পাঠান</string>
|
||||
<string name="custom_zaps_add_a_message_example">আপনার সকল কাজের জন্য আপনাকে অসংখ্য ধন্যবাদ!</string>
|
||||
<string name="lightning_create_and_add_invoice">তৈরি করুন এবং যোগ করুন</string>
|
||||
<string name="poll_author_no_vote">পোল-রচয়িতারা নিজেদের পোলে ভোট দিতে পারবেন না।</string>
|
||||
<string name="hash_verification_passed">ঐ পোস্ট এবং এই আধেয়র বিষয়বস্তু অভিন্ন</string>
|
||||
<string name="hash_verification_failed">এই আধেয়র বিষয়বস্তু পরিবর্তিত হয়েছে। লেখক হয়তো পরিবর্তনটি দেখেন নি বা অনুমোদন করেন নি</string>
|
||||
<string name="content_description_add_image">ছবি যোগ করুন</string>
|
||||
<string name="content_description_add_video">ভিডিও যোগ করুন</string>
|
||||
<string name="content_description_add_document">নথি যোগ করুন</string>
|
||||
<string name="add_content">বার্তায় যোগ করুন</string>
|
||||
<string name="content_description">বিষয়বস্তুর বিবরণ</string>
|
||||
<string name="content_description_example">সূর্যাস্তের সময় সাদা বালুকাময় সমুদ্র সৈকতে একটি নীল নৌকা</string>
|
||||
<string name="zap_type">জ্যাপের ধরন</string>
|
||||
<string name="zap_type_explainer">সকল বিকল্পগুলিতে জ্যাপের ধরন</string>
|
||||
<string name="zap_type_public">সর্বজনীন</string>
|
||||
<string name="zap_type_public_explainer">লেনদেন এবং বার্তা সকলে দেখতে পারেন</string>
|
||||
<string name="zap_type_private">ব্যক্তিগত</string>
|
||||
<string name="zap_type_private_explainer">প্রেরক এবং প্রাপক একে অপরকে দেখতে এবং বার্তা পড়তে পারেন</string>
|
||||
<string name="zap_type_anonymous">অজ্ঞাত-পরিচয়</string>
|
||||
<string name="zap_type_anonymous_explainer">প্রাপক এবং জনসাধারণ জানেন না কে প্রদত্ত অর্থ পাঠিয়েছেন</string>
|
||||
<string name="zap_type_nonzap">নন-জ্যাপ</string>
|
||||
<string name="zap_type_nonzap_explainer">নস্টারে কোনো সন্ধান নেই, কেবল লাইটনিংয়ে আছে</string>
|
||||
<string name="file_server">ফাইল সার্ভার</string>
|
||||
<string name="zap_forward_lnAddress">LN ঠিকানা বা @ইউজার</string>
|
||||
<string name="upload_server_imgur">imgur.com - বিশ্বস্ত</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - বিশ্বস্ত</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_imgur_nip94">যাচাইযোগ্য Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Imgur ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrimg_nip94">যাচাইযোগ্য NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">NostrImg ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrbuild_nip94">যাচাইযোগ্য Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.build ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">যাচাইযোগ্য Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.dev ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">যাচাইযোগ্য Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.me ফাইলটি পরিবর্তন করেছে কিনা তা পরীক্ষা করে। নতুন NIP: অন্য ক্লায়েন্টরা এটি দেখতে নাও পেতে পারে</string>
|
||||
<string name="upload_server_relays_nip95">আপনার রিলেগুলি (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ফাইলগুলো আপনার রিলে দ্বারা গৃহীত হয়। নতুন NIP: তারা এটি সমর্থন করে কিনা দেখে নিন</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot সেটআপ করুন</string>
|
||||
<string name="connect_via_tor">আপনার Orbot সেটআপের মাধ্যমে সংযুক্ত হোন</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Orbot/Tor থেকে সংযোগ বিচ্ছিন্ন করতে চান?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">আপনার ডেটা অবিলম্বে নিয়মিত নেটওয়ার্কে স্থানান্তর করা হবে</string>
|
||||
<string name="yes">হ্যাঁ</string>
|
||||
<string name="no">না</string>
|
||||
<string name="follow_list_selection">অনুসরণ তালিকা</string>
|
||||
<string name="follow_list_kind3follows">অনুসৃত সকলেরা</string>
|
||||
<string name="follow_list_global">বৈশ্বিক</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Tor এর মাধ্যমে Orbot এর সাথে সংযুক্ত হোন
|
||||
\n\n১. [Orbot] নামান (https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n২. Orbot চালু করুন
|
||||
\n৩. Orbot এ ঢুকে Socks পোর্টটি খেয়াল করুন। এটি ৯০৫০ তে নির্দিষ্ট করা থাকে
|
||||
\n৪. প্রয়োজনে Orbot এ পোর্টটি বদলে দিন
|
||||
\n৫. স্ক্রিনের Socks পোর্টটি প্রস্তুত করুন
|
||||
\n৬. Orbot কে প্রক্সি হিসেবে ব্যবহার করতে Activate বোতামটি চাপুন
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot এর Socks পোর্ট</string>
|
||||
<string name="invalid_port_number">পোর্ট নম্বরটি অকার্যকর</string>
|
||||
<string name="use_orbot">Orbot ব্যবহার করুন</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Tor/Orbot থেকে সংযোগ বিচ্ছিন্ন করুন</string>
|
||||
<string name="app_notification_dms_channel_name">ব্যক্তিগত বার্তাসমূহ</string>
|
||||
<string name="app_notification_dms_channel_description">যখন একটি ব্যক্তিগত বার্তা আসে তখন আপনাকে অবহিত করে</string>
|
||||
<string name="app_notification_zaps_channel_name">জ্যাপ গৃহীত হয়েছে</string>
|
||||
<string name="app_notification_zaps_channel_description">কেউ জ্যাপ পাঠালে আপনাকে অবহিত করে</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s স্যাট</string>
|
||||
<string name="app_notification_zaps_channel_message_from">%1$s থেকে</string>
|
||||
<string name="app_notification_zaps_channel_message_for">%1$s এর জন্য</string>
|
||||
<string name="reply_notify">অবহিত করুন: </string>
|
||||
<string name="channel_list_join_conversation">আলোচনায় যুক্ত হোন</string>
|
||||
<string name="channel_list_user_or_group_id">ব্যবহারকারী কিংবা গ্রুপের আইডি</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent অথবা hex</string>
|
||||
<string name="channel_list_create_channel">তৈরি করুন</string>
|
||||
<string name="channel_list_join_channel">যুক্ত হোন</string>
|
||||
<string name="today">আজ</string>
|
||||
<string name="content_warning">আধেয় বিষয়ক সতর্কতা</string>
|
||||
<string name="content_warning_explanation">এই পোস্টে সংবেদনশীল উপাদান রয়েছে যা কারো জন্য আপত্তিকর কিংবা সমস্যাজনক হতে পারে</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">সংবেদনশীল আধেয় সবসময় আড়ালে রাখুন</string>
|
||||
<string name="content_warning_show_all_sensitive_content">সংবেদনশীল বিষয়বস্তু সবসময় খোলামেলা দেখান</string>
|
||||
<string name="content_warning_see_warnings">সবসময় আধেয় বিষয়ক সতর্কতা দেখান</string>
|
||||
<string name="recommended_apps">প্রস্তাবিত: </string>
|
||||
<string name="filter_spam_from_strangers">অপরিচিতদের কাছ থেকে স্প্যাম ফিল্টার করুন</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">কোনো পোস্টে আপনার অনুসরীদের থেকে অভিযোগ থাকলে অবহিত হোন</string>
|
||||
<string name="new_reaction_symbol">প্রতিক্রিয়ার নতুন প্রতীক</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">প্রতিক্রিয়ার কোনো ধরন বেছে নেওয়া হয়নি। পরিবর্তন করতে দীর্ঘক্ষণ চাপুন</string>
|
||||
<string name="zapraiser">জ্যাপরেইজার</string>
|
||||
<string name="zapraiser_explainer">এই পোস্টের জন্য স্যাটের একটি লক্ষ্যমাত্রা যোগ করে। সহায়তাকারী ক্লায়েন্টরা অনুদানকে উৎসাহিত করার জন্য একে একটি অগ্রগতির মানদণ্ড হিসাবে দেখাতে পারে</string>
|
||||
<string name="zapraiser_target_amount_in_sats">পরিমাণের লক্ষ্যমাত্রা স্যাট দ্বারা জানান</string>
|
||||
<string name="sats_to_complete">জ্যাপরাইজার %1$s তে আছে। %2$s স্যাট লক্ষ্যমাত্রা</string>
|
||||
<string name="read_from_relay">রিলে থেকে পড়ুন</string>
|
||||
<string name="write_to_relay">রিলেতে লিখুন</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">%1$s থেকে রিলের তথ্যের জন্য চেষ্টা করার সময় একটি ত্রুটি ঘটেছে৷</string>
|
||||
<string name="owner">মালিক</string>
|
||||
<string name="version">সংস্করণ</string>
|
||||
<string name="software">সফটওয়্যার</string>
|
||||
<string name="contact">যোগাযোগ</string>
|
||||
<string name="supports">সমর্থিত NIP গুলো</string>
|
||||
<string name="admission_fees">প্রবেশমূল্য</string>
|
||||
<string name="payments_url">মূল্যপরিশোধের URL</string>
|
||||
<string name="limitations">সীমাবদ্ধতা</string>
|
||||
<string name="countries">দেশসমূহ</string>
|
||||
<string name="languages">ভাষাসমূহ</string>
|
||||
<string name="tags">ট্যাগগুলি</string>
|
||||
<string name="posting_policy">আধেয় পেশ করার নীতি</string>
|
||||
<string name="message_length">বার্তার ব্যাপ্তি</string>
|
||||
<string name="subscriptions">সকল সদস্যতা</string>
|
||||
<string name="filters">ফিল্টারগুলি</string>
|
||||
<string name="subscription_id_length">সদস্যতা আইডির ব্যাপ্তি</string>
|
||||
<string name="minimum_prefix">ন্যূনতম উপসর্গ</string>
|
||||
<string name="maximum_event_tags">সর্বাধিক ইভেন্ট ট্যাগ</string>
|
||||
<string name="content_length">আধেয়র ব্যাপ্তি</string>
|
||||
<string name="minimum_pow">ন্যূনতম PoW</string>
|
||||
<string name="auth">অনুমোদন</string>
|
||||
<string name="payment">মূল্যপরিশোধ</string>
|
||||
<string name="cashu">ক্যাশু টোকেন</string>
|
||||
<string name="cashu_redeem">খালাস করুন</string>
|
||||
<string name="no_lightning_address_set">কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
|
||||
<string name="copied_token_to_clipboard">টোকেনটি ক্লিপবোর্ডে কপি করা হয়েছে</string>
|
||||
<string name="live_stream_live_tag">লাইভ</string>
|
||||
<string name="live_stream_offline_tag">অফলাইন</string>
|
||||
<string name="live_stream_ended_tag">সমাপ্ত</string>
|
||||
<string name="live_stream_planned_tag">সময়সূচি নির্ধারিত</string>
|
||||
<string name="live_stream_is_offline">লাইভস্ট্রিমটি অফলাইন আছে</string>
|
||||
<string name="live_stream_has_ended">লাইভস্ট্রিম সমাপ্ত</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">লগ আউট করলে আপনার সমস্ত স্থানীয় তথ্য মুছে যাবে। অ্যাকাউন্ট সুরক্ষিত রাখতে আপনার ব্যক্তিগত চাবিটি নিরাপদে সংরক্ষিত আছে কিনা নিশ্চিত হোন। সামনে আগাতে চান?</string>
|
||||
<string name="followed_tags">অনুসৃত ট্যাগগুলি</string>
|
||||
<string name="relay_setup">রিলেগুলি</string>
|
||||
<string name="discover_live">লাইভ</string>
|
||||
<string name="discover_community">কম্যুনিটি</string>
|
||||
<string name="discover_chat">বার্তালাপগুলি</string>
|
||||
<string name="community_approved_posts">অনুমোদিত পোস্টগুলি</string>
|
||||
<string name="groups_no_descriptor">এই দলটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে দলনেতার সাথে কথা বলুন</string>
|
||||
<string name="community_no_descriptor">এই কম্যুনিটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে এর মালিকের সাথে কথা বলুন</string>
|
||||
<string name="add_sensitive_content_label">সংবেদনশীল আধেয়</string>
|
||||
<string name="add_sensitive_content_description">সংবেদনশীল আধেয় দেখানোর আগে এ সম্পর্কিত সতর্কতা প্রদর্শন করে</string>
|
||||
<string name="settings">সেটিংস</string>
|
||||
<string name="connectivity_type_always">সবসময়</string>
|
||||
<string name="connectivity_type_wifi_only">শুধুমাত্র ওয়াইফাইতে</string>
|
||||
<string name="connectivity_type_never">কখনো না</string>
|
||||
<string name="system">সিস্টেম</string>
|
||||
<string name="light">উজ্জ্বল</string>
|
||||
<string name="dark">আঁধারি</string>
|
||||
<string name="application_preferences">অ্যাপ্লিকেশনের পছন্দসমূহ</string>
|
||||
<string name="language">ভাষা</string>
|
||||
<string name="theme">থিম</string>
|
||||
<string name="automatically_load_images_gifs">ছবির পূর্বরূপ</string>
|
||||
<string name="automatically_play_videos">ভিডিও প্লেব্যাক</string>
|
||||
<string name="automatically_show_url_preview">URL-এর পূর্বরূপ</string>
|
||||
<string name="load_image">ছবিটি লোড করুন</string>
|
||||
<string name="spamming_users">স্প্যামাররা</string>
|
||||
<string name="muted_button">মৌন করে রাখা। এটি তুলে নিতে ক্লিক করুন</string>
|
||||
<string name="mute_button">শব্দ চালু আছে। মৌন করতে ক্লিক করুন</string>
|
||||
<string name="search_button">স্থানীয় এবং দূরবর্তী রেকর্ড অনুসন্ধান করুন</string>
|
||||
<string name="nip05_verified">নস্টার ঠিকানাটি যাচাই করা হয়েছে</string>
|
||||
<string name="nip05_failed">নস্টার ঠিকানাটি যাচাই করা যায় নি</string>
|
||||
<string name="nip05_checking">নস্টার ঠিকানাটি পরীক্ষা করে দেখা হচ্ছে</string>
|
||||
<string name="select_deselect_all">সবগুলিকে নির্বাচন/অনির্বাচন করুন</string>
|
||||
<string name="default_relays">পূর্ব-নির্ধারিত</string>
|
||||
<string name="select_a_relay_to_continue">চালিয়ে যেতে একটি রিলে নির্বাচন করুন</string>
|
||||
<string name="zap_forward_title">জ্যাপটি পাঠান:</string>
|
||||
<string name="zap_forward_explainer">সহায়তাকারী ক্লায়েন্টরা আপনার পরিবর্তে নীচের LN ঠিকানা বা ব্যবহারকারীর প্রোফাইলে জ্যাপ পাঠাবে</string>
|
||||
<string name="geohash_title">আপনার অবস্থান এভাবে প্রকাশ করুন </string>
|
||||
<string name="geohash_explainer">পোস্টে আপনার অবস্থানের একটি জিওহ্যাশ যোগ করে। জনসাধারণ জানবে যে আপনি বর্তমান অবস্থান থেকে ৫ কিলোমিটার (৩ মাইল) এর মধ্যে আছেন</string>
|
||||
<string name="add_sensitive_content_explainer">আপনার পেশকৃত আধেয় দেখানোর আগে সংবেদনশীল আধেয়-সতর্কতা জারি করে। এটি সেসকল আধেয়র জন্য আদর্শ যেখানে NSFW উপাদান কিংবা এমনকিছু থাকতে পারে যা কারো কারো জন্য আপত্তিকর কিংবা সমস্যাজনক মনে হতে পারে।</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_title">নতুন ফিচার</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">এই মোড সক্রিয় করার জন্য অ্যামেথিস্টকে একটি NIP-24 বার্তা পাঠাতে হবে। (GiftWrapped, সিল করা সরাসরি এবং দলগত বার্তা)। NIP-24 নতুন এবং বেশিরভাগ ক্লায়েন্ট এখনও এটি বাস্তবায়ন করেনি। নিশ্চিত করুন যে প্রাপক একটি সামঞ্জস্যপূর্ণ ক্লায়েন্ট ব্যবহার করছে।</string>
|
||||
<string name="new_feature_nip24_activate">সক্রিয় করুন</string>
|
||||
<string name="messages_create_public_chat">সর্বজনীন</string>
|
||||
<string name="messages_new_message">ব্যক্তিগত</string>
|
||||
<string name="messages_new_message_to">প্রতি</string>
|
||||
<string name="messages_new_message_subject">বিষয়</string>
|
||||
<string name="messages_new_message_subject_caption">আলোচনার বিষয়</string>
|
||||
<string name="messages_new_message_to_caption">"\@User1, @User2, @User3"</string>
|
||||
<string name="messages_group_descriptor">এই দলের সদস্যরা</string>
|
||||
<string name="messages_new_subject_message">সদস্যদের নিকট ব্যাখ্যা</string>
|
||||
<string name="messages_new_subject_message_placeholder">নতুন লক্ষ্যের জন্য নাম পরিবর্তন করা হচ্ছে।</string>
|
||||
<string name="language_description">অ্যাপের ইন্টারফেসের জন্য</string>
|
||||
<string name="theme_description">আঁধারি, উজ্জ্বল কিংবা সিস্টেমের থিম</string>
|
||||
<string name="automatically_load_images_gifs_description">স্বয়ংক্রিয়ভাবে ছবি এবং GIF লোড করুন</string>
|
||||
<string name="automatically_play_videos_description">স্বয়ংক্রিয়ভাবে ভিডিও এবং GIF চালু করুন</string>
|
||||
<string name="automatically_show_url_preview_description">URL এর পূর্বরূপ দেখান</string>
|
||||
<string name="load_image_description">কখন ছবি লোড করতে হবে</string>
|
||||
<string name="copy_url_to_clipboard">URL ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">নোট আইডি ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="created_at">যেখানে নির্মিত</string>
|
||||
<string name="rules">নিয়ম</string>
|
||||
<string name="status_update">আপনার স্ট্যাটাস আপডেট করুন</string>
|
||||
<string name="lightning_wallets_not_found">Error parsing error message</string>
|
||||
<string name="poll_zap_value_min_max_explainer">জ্যাপের পরিমাণ দ্বারা ভোটসংখ্যা গোণা হয়। আপনি স্প্যামার এড়াতে একটি সর্বনিম্ন পরিমাণ এবং একটি বড়ো জ্যাপার দ্বারা পোল দখল এড়াতে সর্বোচ্চ পরিমাণ স্যাট নির্ধারণ করতে পারেন। প্রতিটি ভোটের মূল্য একই পরিমাণে রয়েছে তা নিশ্চিত করতে উভয় ক্ষেত্রেই একই পরিমাণ ব্যবহার করুন। যেকোনো পরিমাণ গ্রহণ করতে এটি খালি ছেড়ে দিন।</string>
|
||||
<string name="error_dialog_zap_error">জ্যাপ পাঠানো যায় নি</string>
|
||||
<string name="error_dialog_talk_to_user">ব্যবহারকারীকে বার্তা দিন</string>
|
||||
<string name="error_dialog_button_ok">ঠিক আছে</string>
|
||||
<string name="relay_information_document_error_assemble_url">%1$s: %2$s পৌঁছাতে ব্যর্থ</string>
|
||||
<string name="relay_information_document_error_reach_server">%1$s: %2$s পৌঁছাতে ব্যর্থ</string>
|
||||
<string name="relay_information_document_error_parse_result">%1$s: %2$s থেকে ফলাফল পার্স করা যায় নি</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s কোড দিয়ে ব্যর্থ হয়েছে %2$s</string>
|
||||
<string name="active_for">এর জন্য সক্রিয় করুন: </string>
|
||||
<string name="active_for_home">নীড়</string>
|
||||
<string name="active_for_msg">বার্তাসমূহ</string>
|
||||
<string name="active_for_chats">আলাপচারিতা</string>
|
||||
<string name="active_for_global">বৈশ্বিক</string>
|
||||
<string name="active_for_search">অনুসন্ধান করুন</string>
|
||||
<string name="zap_split_title">জ্যাপগুলি ভাগ করুন এবং পাঠান</string>
|
||||
<string name="zap_split_explainer">সমর্থিত ক্লায়েন্টরা জ্যাপ ভাগ করবে এবং আপনার পরিবর্তে এখানে সংযুক্ত ব্যবহারকারীদেরকে পাঠাবে</string>
|
||||
<string name="zap_split_serarch_and_add_user">ব্যবহারকারীদের অনুসন্ধান করুন এবং যুক্ত করুন</string>
|
||||
<string name="zap_split_serarch_and_add_user_placeholder">ইউজারনেম কিংবা প্রদর্শিত নাম দিন</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">%1$s ব্যবহারকারীর স্যাট গ্রহণের উপযোগী কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
|
||||
<string name="zap_split_weight">শতাংশ</string>
|
||||
<string name="zap_split_weight_placeholder">২৫</string>
|
||||
<string name="splitting_zaps_with">এর সাথে জ্যাপ ভাগ করা হচ্ছে</string>
|
||||
<string name="forwarding_zaps_to">একে জ্যাপ পাঠানো হচ্ছে</string>
|
||||
<string name="lightning_wallets_not_found2">লাইটনিং ওয়ালেট পাওয়া যায় নি</string>
|
||||
<string name="paid">পরিশোধিত</string>
|
||||
<string name="wallet_number">ওয়ালেট %1$s</string>
|
||||
</resources>
|
||||
@@ -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">(0–100)%</string>
|
||||
<string name="poll_closing_time">Uzavřít po</string>
|
||||
<string name="poll_closing_time_days">dnech</string>
|
||||
<string name="poll_is_closed">Hlasování je uzavřeno pro nové hlasy</string>
|
||||
<string name="poll_zap_amount">Částka zapsu</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">U tohoto typu hlasování je povolen pouze jeden hlas na uživatele</string>
|
||||
|
||||
<string name="looking_for_event">"Hledá se událost %1$s"</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message">Přidat veřejnou zprávu</string>
|
||||
<string name="custom_zaps_add_a_message_private">Přidat soukromou zprávu</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Přidat fakturu</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">Děkuji za veškerou práci!</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">Vytvořit a přidat</string>
|
||||
<string name="poll_author_no_vote">Autoři hlasování nemohou hlasovat ve vlastních hlasováních.</string>
|
||||
|
||||
<string name="hash_verification_passed">Obrázek je stejný jako v původním příspěvku</string>
|
||||
<string name="hash_verification_failed">Obrázek se změnil. Autor možná změnu nezaznamenal</string>
|
||||
|
||||
<string name="content_description_add_image">Přidat obrázek</string>
|
||||
<string name="content_description_add_video">Přidat video</string>
|
||||
<string name="content_description_add_document">Přidat dokument</string>
|
||||
|
||||
<string name="add_content">Přidat do zprávy</string>
|
||||
<string name="content_description">Popis obsahu</string>
|
||||
<string name="content_description_example">Modrá loď na bílé písčité pláži při západu slunce</string>
|
||||
|
||||
<string name="zap_type">Typ zapsu</string>
|
||||
<string name="zap_type_explainer">Typ zapsu pro všechny možnosti</string>
|
||||
|
||||
<string name="zap_type_public">Veřejný</string>
|
||||
<string name="zap_type_public_explainer">Všichni mohou vidět transakci a zprávu</string>
|
||||
|
||||
<string name="zap_type_private">Soukromý</string>
|
||||
<string name="zap_type_private_explainer">Odesílatel a příjemce si mohou navzájem vidět a číst zprávu</string>
|
||||
|
||||
<string name="zap_type_anonymous">Anonymní</string>
|
||||
<string name="zap_type_anonymous_explainer">Příjemce a veřejnost neví, kdo platbu poslal</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap">Né Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Žádná stopa v Nostr, pouze v Lightning</string>
|
||||
|
||||
<string name="file_server">Souborový server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress nebo @Uživatel</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com – důvěryhodný</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com – důvěryhodný</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build – důvěryhodný</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev – důvěryhodný</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - důvěryhodný</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_imgur_nip94">Ověřitelný Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Ověří, zda Imgur upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Ověřitelný NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Ověří, zda NostrImg upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Ověřitelný Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Ověří, zda Nostr.build upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Ověřitelný Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Ověří, zda Nostrfiles.dev upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">Ověřitelný Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Ověří, zda Nostrcheck.me upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
|
||||
|
||||
<string name="connect_via_tor_short">Nastavení Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Připojit se přes váš Orbot</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Odpojit se od Orbotu/Toru?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Vaše data budou okamžitě přenesena do běžné sítě</string>
|
||||
<string name="yes">Ano</string>
|
||||
<string name="no">Ne</string>
|
||||
|
||||
<string name="follow_list_selection">Seznam sledovaných</string>
|
||||
<string name="follow_list_kind3follows">Všechna sledování</string>
|
||||
<string name="follow_list_global">Globální</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Připojte se přes Tor s Orbotem
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Připojte se přes Tor s Orbotem
|
||||
\n\n1. Nainstalujte [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Spusťte Orbot
|
||||
\n3. V Orbotu zkontrolujte port pro Socks. Výchozí hodnota je 9050
|
||||
@@ -370,54 +334,41 @@
|
||||
\n5. Nastavte port Socks v této obrazovce
|
||||
\n6. Stiskněte tlačítko Aktivovat Tor
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">Neplatné číslo portu</string>
|
||||
<string name="use_orbot">Použít Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Odpojit Tor/Orbot</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">Soukromé zprávy</string>
|
||||
<string name="app_notification_dms_channel_description">Upozorní vás, když dorazí soukromá zpráva</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">Přijaté zapsy</string>
|
||||
<string name="app_notification_zaps_channel_description">Upozorní vás, když vás někdo zapsne</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">Od %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">pro %1$s</string>
|
||||
|
||||
<string name="reply_notify">Upozornit: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Připojit se ke konverzaci</string>
|
||||
<string name="channel_list_user_or_group_id">ID uživatele nebo skupiny</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent nebo hex</string>
|
||||
<string name="channel_list_create_channel">Vytvořit</string>
|
||||
<string name="channel_list_join_channel">Připojit se</string>
|
||||
|
||||
<string name="today">Dnes</string>
|
||||
|
||||
<string name="content_warning">Upozornění na obsah</string>
|
||||
<string name="content_warning_explanation">Tento příspěvek obsahuje citlivý obsah, který může být pro některé lidi urážlivý nebo rušivý</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Vždy skrýt citlivý obsah</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Vždy zobrazit citlivý obsah</string>
|
||||
<string name="content_warning_see_warnings">Vždy zobrazovat upozornění na obsah</string>
|
||||
|
||||
<string name="recommended_apps">Doporučuje: </string>
|
||||
<string name="filter_spam_from_strangers">Filtrovat spam od cizinců</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob</string>
|
||||
|
||||
<string name="new_reaction_symbol">Nový symbol reakce</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Nebyly vybrány žádné typy reakcí. Dlouhým stiskem změňte</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser">Zap-sběr</string>
|
||||
<string name="zapraiser_explainer">Přidá cílovou částku sats pro tento příspěvek. Podporované klienty ji mohou zobrazovat jako ukazatel postupu k podněcování darování</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Cílová částka v sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser na %1$s. Do cíle zbývá %2$s sats</string>
|
||||
<string name="read_from_relay">Číst z Relay</string>
|
||||
<string name="write_to_relay">Zapisovat do Relay</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string>
|
||||
<string name="owner">Vlastník</string>
|
||||
<string name="version">Verze</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="supports">Podporované NIPs</string>
|
||||
<string name="admission_fees">Vstupné</string>
|
||||
@@ -437,38 +388,29 @@
|
||||
<string name="minimum_pow">Minimální PoW</string>
|
||||
<string name="auth">Ověření</string>
|
||||
<string name="payment">Platba</string>
|
||||
<string name="cashu">Cashu Token</string>
|
||||
<string name="cashu_redeem">Vyměnit</string>
|
||||
<string name="no_lightning_address_set">Není nastavena žádná Lightning adresa</string>
|
||||
<string name="copied_token_to_clipboard">Token zkopírován do schránky</string>
|
||||
|
||||
<string name="live_stream_live_tag">ŽIVĚ</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">UKONČENO</string>
|
||||
<string name="live_stream_planned_tag">PLÁNOVÁNO</string>
|
||||
|
||||
<string name="live_stream_is_offline">Živé vysílání je offline</string>
|
||||
<string name="live_stream_has_ended">Živé vysílání ukončeno</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Odhlášení vymaže všechny vaše místní informace. Ujistěte se, že máte zálohované své privátní klíče, abyste se vyhnuli ztrátě účtu. Chcete pokračovat?</string>
|
||||
<string name="followed_tags">Sledované značky</string>
|
||||
|
||||
<string name="relay_setup">Rele</string>
|
||||
|
||||
<string name="discover_live">Živě</string>
|
||||
<string name="discover_community">Komunita</string>
|
||||
<string name="discover_chat">Chaty</string>
|
||||
<string name="community_approved_posts">Schválené příspěvky</string>
|
||||
|
||||
<string name="groups_no_descriptor">Tato skupina nemá popis ani pravidla. Promluvte si s majitelem, aby je přidal/a.</string>
|
||||
<string name="community_no_descriptor">Tato komunita nemá popis. Promluvte si s majitelem, aby ho přidal/a.</string>
|
||||
|
||||
<string name="add_sensitive_content_label">Citlivý obsah</string>
|
||||
<string name="add_sensitive_content_description">Před zobrazením tohoto obsahu přidá upozornění na citlivý obsah.</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="connectivity_type_always">Vždy</string>
|
||||
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
|
||||
<string name="connectivity_type_never">Nikdy</string>
|
||||
|
||||
<string name="system">Systém</string>
|
||||
<string name="light">Světlý</string>
|
||||
<string name="dark">Tmavý</string>
|
||||
@@ -479,47 +421,56 @@
|
||||
<string name="automatically_play_videos">Automaticky přehrávat videa</string>
|
||||
<string name="automatically_show_url_preview">Automaticky zobrazit náhled URL</string>
|
||||
<string name="load_image">Načíst obrázek</string>
|
||||
|
||||
<string name="spamming_users">Spamovací uživatelé</string>
|
||||
|
||||
<string name="muted_button">Ztlumené. Klikněte pro odztlumení</string>
|
||||
<string name="mute_button">Zvuk zapnutý. Klikněte pro ztlumení</string>
|
||||
<string name="search_button">Hledat lokální a vzdálené záznamy</string>
|
||||
|
||||
<string name="nip05_verified">Adresa Nostr byla ověřena</string>
|
||||
<string name="nip05_failed">Ověření adresy Nostr se nezdařilo</string>
|
||||
<string name="nip05_checking">Kontrola adresy Nostr</string>
|
||||
<string name="select_deselect_all">Vybrat/Zrušit vše</string>
|
||||
<string name="default_relays">Výchozí</string>
|
||||
<string name="select_a_relay_to_continue">Vyberte relé pro pokračování</string>
|
||||
|
||||
<string name="zap_forward_title">Přeposílat Zapy na:</string>
|
||||
<string name="zap_forward_explainer">Podporující klienti budou přeposílat Zapy na níže uvedenou LN adresu nebo uživatelský profil místo vaší</string>
|
||||
|
||||
<string name="geohash_title">Zveřejnit polohu jako </string>
|
||||
<string name="geohash_explainer">Přidá Geohash vaší polohy do příspěvku. Veřejnost bude vědět, že se nacházíte do 5 km od aktuální polohy</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Nová funkce</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-24 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-24 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta.</string>
|
||||
<string name="new_feature_nip24_activate">Aktivovat</string>
|
||||
|
||||
<string name="messages_create_public_chat">Veřejné</string>
|
||||
<string name="messages_new_message">Soukromé</string>
|
||||
<string name="messages_new_message_to">Pro</string>
|
||||
<string name="messages_new_message_subject">Předmět</string>
|
||||
<string name="messages_new_message_subject_caption">Téma konverzace</string>
|
||||
<string name="messages_new_message_to_caption">"@Uživatel1, @Uživatel2, @Uživatel3"</string>
|
||||
|
||||
<string name="messages_new_message_to_caption">"\@Uživatel1, @Uživatel2, @Uživatel3"</string>
|
||||
<string name="messages_group_descriptor">Členové této skupiny</string>
|
||||
<string name="messages_new_subject_message">Vysvětlení členům</string>
|
||||
<string name="messages_new_subject_message_placeholder">Změna názvu pro nové cíle.</string>
|
||||
|
||||
<string name="language_description">Pro rozhraní aplikace</string>
|
||||
<string name="theme_description">Tmavé, světlé nebo systémové téma</string>
|
||||
<string name="automatically_load_images_gifs_description">Automaticky načítat obrázky a GIFy</string>
|
||||
<string name="automatically_play_videos_description">Automaticky přehrávat videa a GIFy</string>
|
||||
<string name="automatically_show_url_preview_description">Zobrazit náhledy URL</string>
|
||||
<string name="load_image_description">Kdy načíst obrázek</string>
|
||||
<string name="copy_url_to_clipboard">Kopírovat URL do schránky</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Kopírovat ID poznámky do schránky</string>
|
||||
<string name="created_at">Vytvořeno</string>
|
||||
<string name="rules">Pravidla</string>
|
||||
<string name="status_update">Aktualizovat svůj stav</string>
|
||||
<string name="lightning_wallets_not_found">Chyba při zpracování chybové zprávy</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Hlasy jsou váženy podle hodnoty zapu. Můžete nastavit minimální částku, abyste se vyhnuli spammerům, a maximální částku, abyste zabránili velkým zapperům, kteří by mohli ovládnout hlasování. Použijte stejnou částku v obou polích, aby byla hodnota každého hlasu stejná. Nechte prázdné pro přijetí libovolné částky.</string>
|
||||
<string name="error_dialog_zap_error">Nelze odeslat zap</string>
|
||||
<string name="error_dialog_talk_to_user">Poslat zprávu uživateli</string>
|
||||
<string name="relay_information_document_error_assemble_url">Nepodařilo se dosáhnout %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Nepodařilo se dosáhnout %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Nepodařilo se zpracovat výsledek z %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s selhalo s kódem %2$s</string>
|
||||
<string name="active_for">Aktivní pro: </string>
|
||||
<string name="active_for_home">Domů</string>
|
||||
<string name="active_for_msg">Zprávy</string>
|
||||
<string name="active_for_chats">Chaty</string>
|
||||
<string name="active_for_global">Celosvětové</string>
|
||||
<string name="active_for_search">Hledání</string>
|
||||
</resources>
|
||||
|
||||
2
app/src/main/res/values-cy-rGB/strings.xml
Normal file
2
app/src/main/res/values-cy-rGB/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
2
app/src/main/res/values-da-rDK/strings.xml
Normal file
2
app/src/main/res/values-da-rDK/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
@@ -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">(0–100)%</string>
|
||||
<string name="poll_closing_time">Schließen nach</string>
|
||||
<string name="poll_closing_time_days">Tagen</string>
|
||||
<string name="poll_is_closed">Umfrage ist für neue Stimmen geschlossen</string>
|
||||
<string name="poll_zap_amount">Zap-Betrag</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Es ist nur eine Stimme pro Benutzer für diesen Umfragetyp erlaubt</string>
|
||||
|
||||
<string name="looking_for_event">Veranstaltung suchen</string>
|
||||
<string name="looking_for_event">"Veranstaltung suchen"</string>
|
||||
<string name="custom_zaps_add_a_message">Füge eine öffentliche Nachricht hinzu</string>
|
||||
<string name="custom_zaps_add_a_message_private">Füge eine private Nachricht hinzu</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Füge eine Rechnungsnachricht hinzu</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">Vielen Dank für deine Arbeit!</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">Erstellen und Hinzufügen</string>
|
||||
<string name="poll_author_no_vote">Umfragenautoren können nicht in ihren eigenen Umfragen abstimmen.</string>
|
||||
|
||||
<string name="hash_verification_passed">Das Bild ist seitdem Beitrag dasselbe</string>
|
||||
<string name="hash_verification_failed">Das Bild hat sich geändert. Der Autor hat die Änderung möglicherweise nicht bemerkt.</string>
|
||||
|
||||
<string name="content_description_add_image">Bild hinzufügen</string>
|
||||
<string name="content_description_add_video">Video hinzufügen</string>
|
||||
<string name="content_description_add_document">Dokument hinzufügen</string>
|
||||
|
||||
<string name="add_content">Zur Nachricht hinzufügen</string>
|
||||
<string name="content_description">Beschreibung des Inhalts</string>
|
||||
<string name="content_description_example">Ein blaues Boot an einem weißen Sandstrand bei Sonnenuntergang</string>
|
||||
|
||||
<string name="zap_type">Zap-Typ</string>
|
||||
<string name="zap_type_explainer">Zap-Typ für alle Optionen</string>
|
||||
|
||||
<string name="zap_type_public">Öffentlich</string>
|
||||
<string name="zap_type_public_explainer">Jeder kann die Transaktion und die Nachricht sehen</string>
|
||||
|
||||
<string name="zap_type_private">Privat</string>
|
||||
<string name="zap_type_private_explainer">Absender und Empfänger können einander sehen und die Nachricht lesen</string>
|
||||
|
||||
<string name="zap_type_anonymous">Anonym</string>
|
||||
<string name="zap_type_anonymous_explainer">Empfänger und die Öffentlichkeit wissen nicht, wer die Zahlung gesendet hat</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap">Keine Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Keine Spur in Nostr, nur in Lightning</string>
|
||||
|
||||
|
||||
<string name="file_server">Dateiserver</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress oder @Benutzer</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - vertrauenswürdig</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur kann die Datei ändern</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg kann die Datei ändern</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build kann die Datei ändern</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kann die Datei ändern</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - vertrauenswürdig</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kann die Datei ändern</string>
|
||||
|
||||
<string name="upload_server_imgur_nip94">Verifizierbares Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Überprüft, ob Imgur die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Verifizierbares NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Überprüft, ob NostrImg die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Verifizierbares Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Überprüft, ob Nostr.build die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifizierbares Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Überprüft, ob Nostrfiles.dev die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifizierbares Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Überprüft, ob Nostrcheck.me die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">Deine Relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Dateien werden von deinen Relays gehostet. Neuer NIP: Überprüfe, ob sie unterstützt werden</string>
|
||||
|
||||
<string name="connect_via_tor_short">Tor/Orbot-Einrichtung</string>
|
||||
<string name="connect_via_tor">Verbindung über deine Orbot-Einrichtung herstellen</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Von deiner Orbot/Tor trennen?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Deine Daten werden sofort im regulären Netzwerk übertragen</string>
|
||||
<string name="yes">Ja</string>
|
||||
<string name="no">Nein</string>
|
||||
|
||||
|
||||
<string name="follow_list_selection">Folgen-Liste</string>
|
||||
<string name="follow_list_kind3follows">Alle Folgen</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Über Tor mit Orbot verbinden
|
||||
<string name="follow_list_global">Weltweit</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Über Tor mit Orbot verbinden
|
||||
\n\n1. Installiere [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Starte Orbot
|
||||
\n3. Überprüfe in Orbot den Socks-Port. Standardmäßig wird der Port 9050 verwendet
|
||||
\n4. Ändere gegebenenfalls den Port in Orbot
|
||||
\n5. Konfiguriere den Socks-Port in diesem Bildschirm
|
||||
\n6. Drücke die Aktivieren-Schaltfläche, um Orbot als Proxy zu verwenden
|
||||
</string>
|
||||
\n6. Drücke die Aktivieren-Schaltfläche, um Orbot als Proxy zu verwenden</string>
|
||||
<string name="orbot_socks_port">Orbot Socks-Port</string>
|
||||
<string name="invalid_port_number">Ungültige Portnummer</string>
|
||||
<string name="use_orbot">Orbot verwenden</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Tor/Orbot trennen</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">Private Nachrichten</string>
|
||||
<string name="app_notification_dms_channel_description">Benachrichtigt dich, wenn eine private Nachricht eingeht</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">Erhaltene Zaps</string>
|
||||
<string name="app_notification_zaps_channel_description">Benachrichtigt dich, wenn dich jemand zappt</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s Sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">Von %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">für %1$s</string>
|
||||
|
||||
<string name="reply_notify">Benachrichtigen: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Unterhaltung beitreten</string>
|
||||
<string name="channel_list_user_or_group_id">Benutzer- oder Gruppen-ID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent oder hex</string>
|
||||
<string name="channel_list_create_channel">Erstellen</string>
|
||||
<string name="channel_list_join_channel">Beitreten</string>
|
||||
|
||||
<string name="today">Heute</string>
|
||||
|
||||
<string name="content_warning">Inhaltswarnung</string>
|
||||
<string name="content_warning_explanation">Dieser Beitrag enthält sensiblen Inhalt, den einige Personen als beleidigend oder verstörend empfinden könnten</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Sensiblen Inhalt immer ausblenden</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Sensiblen Inhalt immer anzeigen</string>
|
||||
<string name="content_warning_see_warnings">Immer Inhaltswarnungen anzeigen</string>
|
||||
|
||||
<string name="recommended_apps">Empfohlene Apps: </string>
|
||||
|
||||
<string name="filter_spam_from_strangers">Spam von Fremden filtern</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Warnung bei Meldungen von deinen Abonnements</string>
|
||||
|
||||
<string name="new_reaction_symbol">Neues Reaktionssymbol</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Keine Reaktionstypen ausgewählt. Lange drücken, um zu ändern</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser">Zap-Sammlung</string>
|
||||
<string name="zapraiser_explainer">Fügt einen Zielbetrag in Sats für diesen Beitrag hinzu. Unterstützende Clients können dies als Fortschrittsbalken anzeigen, um Spenden zu fördern</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Zielbetrag in Sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser bei %1$s. %2$s Sats bis zum Ziel</string>
|
||||
<string name="read_from_relay">Von Relay lesen</string>
|
||||
<string name="write_to_relay">In Relay schreiben</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string>
|
||||
<string name="owner">Inhaber</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="supports">Unterstützte NIPs</string>
|
||||
<string name="admission_fees">Eintrittsgebühren</string>
|
||||
@@ -434,7 +379,6 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="limitations">Einschränkungen</string>
|
||||
<string name="countries">Länder</string>
|
||||
<string name="languages">Sprachen</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="posting_policy">Veröffentlichungsrichtlinie</string>
|
||||
<string name="message_length">Nachrichtenlänge</string>
|
||||
<string name="subscriptions">Abonnements</string>
|
||||
@@ -450,35 +394,22 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="cashu_redeem">Einlösen</string>
|
||||
<string name="no_lightning_address_set">Keine Lightning-Adresse festgelegt</string>
|
||||
<string name="copied_token_to_clipboard">Token in die Zwischenablage kopiert</string>
|
||||
|
||||
<string name="live_stream_live_tag">LIVE</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">BEENDET</string>
|
||||
<string name="live_stream_planned_tag">GEPLANT</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream ist offline</string>
|
||||
<string name="live_stream_has_ended">Livestream beendet</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Das Abmelden löscht alle Ihre lokalen Informationen. Stellen Sie sicher, dass Sie Ihre privaten Schlüssel gesichert haben, um einen Kontoverlust zu vermeiden. Möchten Sie fortfahren?</string>
|
||||
<string name="followed_tags">Gefolgte Tags</string>
|
||||
|
||||
<string name="relay_setup">Relais</string>
|
||||
|
||||
<string name="discover_live">Live</string>
|
||||
<string name="discover_community">Community</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Genehmigte Beiträge</string>
|
||||
|
||||
<string name="groups_no_descriptor">Diese Gruppe hat keine Beschreibung oder Regeln. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen.</string>
|
||||
<string name="community_no_descriptor">Diese Community hat keine Beschreibung. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen.</string>
|
||||
|
||||
<string name="add_sensitive_content_label">Sensibler Inhalt</string>
|
||||
<string name="add_sensitive_content_description">Fügt eine Warnung für sensiblen Inhalt hinzu, bevor dieser Inhalt angezeigt wird.</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="connectivity_type_always">Immer</string>
|
||||
<string name="connectivity_type_wifi_only">Nur WLAN</string>
|
||||
<string name="connectivity_type_never">Nie</string>
|
||||
|
||||
<string name="system">System</string>
|
||||
<string name="light">Hell</string>
|
||||
<string name="dark">Dunkel</string>
|
||||
<string name="application_preferences">Anwendungseinstellungen</string>
|
||||
@@ -488,47 +419,54 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="automatically_play_videos">Videos automatisch abspielen</string>
|
||||
<string name="automatically_show_url_preview">URL-Vorschau automatisch anzeigen</string>
|
||||
<string name="load_image">Bild laden</string>
|
||||
|
||||
<string name="spamming_users">Spammer</string>
|
||||
|
||||
<string name="muted_button">Ton aus. Klicken, um Ton einzuschalten</string>
|
||||
<string name="mute_button">Ton an. Klicken, um Ton auszuschalten</string>
|
||||
<string name="search_button">Lokale und entfernte Einträge durchsuchen</string>
|
||||
|
||||
<string name="nip05_verified">Nostr-Adresse verifiziert</string>
|
||||
<string name="nip05_failed">Nostr-Adresse konnte nicht verifiziert werden</string>
|
||||
<string name="nip05_checking">Nostr-Adresse wird überprüft</string>
|
||||
<string name="select_deselect_all">Alle auswählen/abwählen</string>
|
||||
<string name="default_relays">Standard</string>
|
||||
<string name="select_a_relay_to_continue">Wählen Sie ein Relais aus, um fortzufahren</string>
|
||||
|
||||
<string name="zap_forward_title">Weiterleiten von Zaps an:</string>
|
||||
<string name="zap_forward_explainer">Unterstützende Clients leiten Zaps an die LNAddress oder das Benutzerprofil unten weiter, anstatt an Ihre eigene Adresse</string>
|
||||
|
||||
<string name="geohash_title">Ort preisgeben als </string>
|
||||
<string name="geohash_explainer">Fügt dem Beitrag einen Geohash Ihres Standorts hinzu. Die Öffentlichkeit wird wissen, dass Sie sich innerhalb von 5 km (3 mi) vom aktuellen Standort befinden</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Neues Feature</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Um diesen Modus zu aktivieren, muss Amethyst eine NIP-24-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-24 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet.</string>
|
||||
<string name="new_feature_nip24_activate">Aktivieren</string>
|
||||
|
||||
<string name="messages_create_public_chat">Öffentlich</string>
|
||||
<string name="messages_new_message">Privat</string>
|
||||
<string name="messages_new_message_to">An</string>
|
||||
<string name="messages_new_message_subject">Betreff</string>
|
||||
<string name="messages_new_message_subject_caption">Gesprächsthema</string>
|
||||
<string name="messages_new_message_to_caption">"@Benutzer1, @Benutzer2, @Benutzer3"</string>
|
||||
|
||||
<string name="messages_new_message_to_caption">"\@Benutzer1, @Benutzer2, @Benutzer3"</string>
|
||||
<string name="messages_group_descriptor">Mitglieder dieser Gruppe</string>
|
||||
<string name="messages_new_subject_message">Erklärung an Mitglieder</string>
|
||||
<string name="messages_new_subject_message_placeholder">Ändern des Namens für die neuen Ziele.</string>
|
||||
|
||||
<string name="language_description">Für die App-Benutzeroberfläche</string>
|
||||
<string name="theme_description">Dunkles, helles oder Systemdesign</string>
|
||||
<string name="automatically_load_images_gifs_description">Bilder und GIFs automatisch laden</string>
|
||||
<string name="automatically_play_videos_description">Videos und GIFs automatisch abspielen</string>
|
||||
<string name="automatically_show_url_preview_description">URL-Vorschauen anzeigen</string>
|
||||
<string name="load_image_description">Wann Bilder geladen werden sollen</string>
|
||||
</resources>
|
||||
<string name="copy_url_to_clipboard">URL in die Zwischenablage kopieren</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Notiz-ID in die Zwischenablage kopieren</string>
|
||||
<string name="created_at">Erstellt am</string>
|
||||
<string name="rules">Regeln</string>
|
||||
<string name="status_update">Status aktualisieren</string>
|
||||
<string name="lightning_wallets_not_found">Fehler beim Verarbeiten der Fehlermeldung</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Die Abstimmungen werden nach der Höhe des Zaps gewichtet. Sie können einen Mindestbetrag festlegen, um Spam zu verhindern, und einen Höchstbetrag, um zu verhindern, dass große Zapper die Abstimmung dominieren. Verwenden Sie denselben Betrag in beiden Feldern, um sicherzustellen, dass jeder Stimme der gleiche Wert zukommt. Lassen Sie es leer, um jeden Betrag zu akzeptieren.</string>
|
||||
<string name="error_dialog_zap_error">Zap konnte nicht gesendet werden</string>
|
||||
<string name="error_dialog_talk_to_user">Mit dem Benutzer kommunizieren</string>
|
||||
<string name="relay_information_document_error_assemble_url">Konnte %1$s nicht erreichen: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Konnte %1$s nicht erreichen: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Konnte das Ergebnis von %1$s nicht verarbeiten: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s fehlgeschlagen mit Code %2$s</string>
|
||||
<string name="active_for">Aktiv für: </string>
|
||||
<string name="active_for_home">Startseite</string>
|
||||
<string name="active_for_msg">Nachrichten</string>
|
||||
<string name="active_for_search">Suche</string>
|
||||
</resources>
|
||||
|
||||
2
app/src/main/res/values-el-rGR/strings.xml
Normal file
2
app/src/main/res/values-el-rGR/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
2
app/src/main/res/values-en-rGB/strings.xml
Normal file
2
app/src/main/res/values-en-rGB/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
@@ -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">"<Privata mesaĝo nemalĉifrebla.>\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">"<Privata mesaĝo nemalĉifrebla.>\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>
|
||||
|
||||
189
app/src/main/res/values-es-rES/strings.xml
Normal file
189
app/src/main/res/values-es-rES/strings.xml
Normal 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>
|
||||
19
app/src/main/res/values-es-rMX/strings.xml
Normal file
19
app/src/main/res/values-es-rMX/strings.xml
Normal 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>
|
||||
2
app/src/main/res/values-es-rUS/strings.xml
Normal file
2
app/src/main/res/values-es-rUS/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
2
app/src/main/res/values-et-rEE/strings.xml
Normal file
2
app/src/main/res/values-et-rEE/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
@@ -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></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/<user>/<gist></string>
|
||||
<string name="telegram_proof_url_template" translatable="false">https://t.me/<proof post></string>
|
||||
<string name="mastodon_proof_url_template" translatable="false">https://<server>/<user>/<proof post></string>
|
||||
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/<user>/status/<proof post></string>
|
||||
<string name="private_conversation_notification">"<نمی توان پیام خصوصی را رمزگشایی کردن>\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>
|
||||
|
||||
487
app/src/main/res/values-fi-rFI/strings.xml
Normal file
487
app/src/main/res/values-fi-rFI/strings.xml
Normal file
@@ -0,0 +1,487 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">Osoita QR-koodiin</string>
|
||||
<string name="show_qr">Näytä QR</string>
|
||||
<string name="profile_image">Profiilikuva</string>
|
||||
<string name="scan_qr">Skannaa QR</string>
|
||||
<string name="show_anyway">Näytä silti</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Viesti raportoitiin käyttäjän toimesta</string>
|
||||
<string name="post_not_found">Tapahtumaa ladataan tai sitä ei löydy relelistalta</string>
|
||||
<string name="channel_image">Kanavan kuva</string>
|
||||
<string name="referenced_event_not_found">Viitattua tapahtumaa ei löytynyt</string>
|
||||
<string name="could_not_decrypt_the_message">Viestin purku epäonnistui</string>
|
||||
<string name="group_picture">Ryhmäkuva</string>
|
||||
<string name="explicit_content">Julkeaa sisältöä</string>
|
||||
<string name="spam">Roskaposti</string>
|
||||
<string name="impersonation">Väärennös</string>
|
||||
<string name="illegal_behavior">Laiton toiminta</string>
|
||||
<string name="unknown">Tuntematon</string>
|
||||
<string name="relay_icon">Relekuvake</string>
|
||||
<string name="unknown_author">Tuntematon tekijä</string>
|
||||
<string name="copy_text">Kopioi teksti</string>
|
||||
<string name="copy_user_pubkey">Kopioi tekijän tunniste</string>
|
||||
<string name="copy_note_id">Kopioi viestin tunniste</string>
|
||||
<string name="broadcast">Lähetä</string>
|
||||
<string name="request_deletion">Pyydä poistoa</string>
|
||||
<string name="block_report">Estä / Ilmoita</string>
|
||||
<string name="block_hide_user"><![CDATA[Estä & Piilota käyttäjä]]></string>
|
||||
<string name="report_spam_scam">Ilmoita roskapostista / huijauksesta</string>
|
||||
<string name="report_impersonation">Ilmoita väärennöksestä</string>
|
||||
<string name="report_explicit_content">Ilmoita julkeasta sisällöstä</string>
|
||||
<string name="report_illegal_behaviour">Ilmoita laittomasta toiminnasta</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Kirjaudu sisään yksityisellä avaimella viestehin vastaamiseksi</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Kirjaudu sisään yksityisellä avaimella tehostaaksesi viestejä</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Kirjaudu sisään yksityisellä avaimella tykätäksesi viesteistä</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Zap-määrää ei ole asetettu. Pidä painiketta pohjassa vaihtaaksesi sitä</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Kirjaudu sisään yksityisellä avaimella Zapien lähettämiseksi</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">Kirjaudu sisään yksityisellä avaimella voidaksesi seurata</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Kirjaudu sisään yksityisellä avaimella seuraamisen lopettamiseksi</string>
|
||||
<string name="zaps">Zapit</string>
|
||||
<string name="view_count">Katselukerrat</string>
|
||||
<string name="boost">Tehosta</string>
|
||||
<string name="boosted">tehostettu</string>
|
||||
<string name="quote">Lainaa</string>
|
||||
<string name="new_amount_in_sats">Uusi määrä satseissa</string>
|
||||
<string name="add">Lisää</string>
|
||||
<string name="replying_to">"vastaus kohteeseen "</string>
|
||||
<string name="and">" ja "</string>
|
||||
<string name="in_channel">"kanavassa "</string>
|
||||
<string name="profile_banner">Profiilin banneri</string>
|
||||
<string name="payment_successful">Maksu onnistui</string>
|
||||
<string name="error_parsing_error_message">Virhe virhesanomaa tulkittaessa</string>
|
||||
<string name="following">" Seuratut"</string>
|
||||
<string name="followers">" Seuraajat"</string>
|
||||
<string name="profile">Profiili</string>
|
||||
<string name="security_filters">Turvallisuussuodattimet</string>
|
||||
<string name="log_out">Kirjaudu ulos</string>
|
||||
<string name="show_more">Näytä lisää</string>
|
||||
<string name="lightning_invoice">Lightning lasku</string>
|
||||
<string name="pay">Maksa</string>
|
||||
<string name="lightning_tips">Lightning tipit</string>
|
||||
<string name="note_to_receiver">Viesti vastaanottajalle</string>
|
||||
<string name="thank_you_so_much">Kiitos paljon!</string>
|
||||
<string name="amount_in_sats">Määrä satseissa</string>
|
||||
<string name="send_sats">Lähetä satseja</string>
|
||||
<string name="error_parsing_preview_for">"Virhe tulkittaessa esikatselua kohteelle %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Esikatsele kortin kuvaa kohteelle %1$s"</string>
|
||||
<string name="new_channel">Uusi kanava</string>
|
||||
<string name="channel_name">Kanavan nimi</string>
|
||||
<string name="my_awesome_group">Oma upea ryhmäni</string>
|
||||
<string name="picture_url">Kuvan URL</string>
|
||||
<string name="description">Kuvaus</string>
|
||||
<string name="about_us">"Tietoa meistä.. "</string>
|
||||
<string name="what_s_on_your_mind">Mitä mielessä?</string>
|
||||
<string name="post">Lähetä</string>
|
||||
<string name="save">Tallenna</string>
|
||||
<string name="create">Luo</string>
|
||||
<string name="cancel">Peruuta</string>
|
||||
<string name="failed_to_upload_the_image">Kuvan lataus epäonnistui</string>
|
||||
<string name="relay_address">Releosoite</string>
|
||||
<string name="posts">Viestit</string>
|
||||
<string name="bytes">Tavua</string>
|
||||
<string name="errors">Virheet</string>
|
||||
<string name="home_feed">Kotisyöte</string>
|
||||
<string name="private_message_feed">Yksityinen viestisyöte</string>
|
||||
<string name="public_chat_feed">Julkinen chat-syöte</string>
|
||||
<string name="global_feed">Maailmanlaajuinen syöte</string>
|
||||
<string name="search_feed">Hae syötettä</string>
|
||||
<string name="add_a_relay">Lisää rele</string>
|
||||
<string name="display_name">Näyttönimi</string>
|
||||
<string name="my_display_name">Oma näyttönimeni</string>
|
||||
<string name="username">Käyttäjänimi</string>
|
||||
<string name="my_username">Oma käyttäjänimeni</string>
|
||||
<string name="about_me">Tietoja minusta</string>
|
||||
<string name="avatar_url">Avatarin URL</string>
|
||||
<string name="banner_url">Bannerin URL</string>
|
||||
<string name="website_url">Verkkosivuston URL</string>
|
||||
<string name="ln_address">LN-osoite</string>
|
||||
<string name="ln_url_outdated">LN-URL (vanhentunut)</string>
|
||||
<string name="image_saved_to_the_gallery">Kuva tallennettu galleriaan</string>
|
||||
<string name="failed_to_save_the_image">Kuvan tallennus epäonnistui</string>
|
||||
<string name="upload_image">Lataa kuva</string>
|
||||
<string name="uploading">Ladataan…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Käyttäjällä ei ole asetettuna Lightning-osoitetta satsien vastaanottamiseksi</string>
|
||||
<string name="reply_here">"vastaa tähän.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Kopioi viestin tunniste leikepöydälle Nostressa jakamista varten</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Kopioi kanavan tunniste (viesti) leikepöydälle</string>
|
||||
<string name="edits_the_channel_metadata">Muokkaa kanavan metatietoja</string>
|
||||
<string name="join">Liity</string>
|
||||
<string name="known">Tunnetut</string>
|
||||
<string name="new_requests">Uudet pyynnöt</string>
|
||||
<string name="blocked_users">Estetyt käyttäjät</string>
|
||||
<string name="new_threads">Uudet keskustelut</string>
|
||||
<string name="conversations">Keskustelut</string>
|
||||
<string name="notes">Viestit</string>
|
||||
<string name="replies">Vastaukset</string>
|
||||
<string name="follows">"Seuratut"</string>
|
||||
<string name="reports">"Ilmiannetut"</string>
|
||||
<string name="more_options">Lisää vaihtoehtoja</string>
|
||||
<string name="relays">" Releet"</string>
|
||||
<string name="website">Verkkosivusto</string>
|
||||
<string name="lightning_address">Lightning-osoite</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Kopioi Nsec-tunniste (salasanasi) leikepöydälle varmuuskopiointia varten</string>
|
||||
<string name="copy_private_key_to_the_clipboard">Kopioi yksityinen avain leikepöydälle</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Kopioi julkinen avain leikepöydälle jakamista varten</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Kopioi julkinen avain (NPub) leikepöydälle</string>
|
||||
<string name="send_a_direct_message">Lähetä suora viesti</string>
|
||||
<string name="edits_the_user_s_metadata">Muokkaa käyttäjän metatietoja</string>
|
||||
<string name="follow">Seuraa</string>
|
||||
<string name="follow_back">Seuraa takaisin</string>
|
||||
<string name="unblock">Poista esto</string>
|
||||
<string name="copy_user_id">Kopioi käyttäjän tunniste</string>
|
||||
<string name="unblock_user">Poista käyttäjän esto</string>
|
||||
<string name="npub_hex_username">"npub, käyttäjänimi, teksti"</string>
|
||||
<string name="clear">Tyhjennä</string>
|
||||
<string name="app_logo">Sovelluksen logo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec.. tai npub..</string>
|
||||
<string name="show_password">Näytä salasana</string>
|
||||
<string name="hide_password">Piilota salasana</string>
|
||||
<string name="invalid_key">Virheellinen avain</string>
|
||||
<string name="i_accept_the">"Hyväksyn "</string>
|
||||
<string name="terms_of_use">käyttöehdot</string>
|
||||
<string name="acceptance_of_terms_is_required">Käyttöehtojen hyväksyminen on pakollista</string>
|
||||
<string name="key_is_required">Avain on pakollinen</string>
|
||||
<string name="login">Kirjaudu sisään</string>
|
||||
<string name="generate_a_new_key">Luo uusi avain</string>
|
||||
<string name="loading_feed">Ladataan syöte</string>
|
||||
<string name="error_loading_replies">"Virhe ladattaessa vastauksia: "</string>
|
||||
<string name="try_again">Yritä uudelleen</string>
|
||||
<string name="feed_is_empty">Syöte on tyhjä.</string>
|
||||
<string name="refresh">Päivitä</string>
|
||||
<string name="created">luotu</string>
|
||||
<string name="with_description_of">kuvaus</string>
|
||||
<string name="and_picture">ja kuva</string>
|
||||
<string name="changed_chat_name_to">muutti keskustelun nimen kohteeksi</string>
|
||||
<string name="description_to">kuvaus kohteeksi</string>
|
||||
<string name="and_picture_to">ja kuva kohteeksi</string>
|
||||
<string name="leave">Poistu</string>
|
||||
<string name="unfollow">Lopeta seuraaminen</string>
|
||||
<string name="channel_created">Kanava luotu</string>
|
||||
<string name="channel_information_changed_to">"Kanavan tiedot muutettu kohteeksi"</string>
|
||||
<string name="public_chat">Julkinen keskustelu</string>
|
||||
<string name="posts_received">vastaanotetut viestit</string>
|
||||
<string name="remove">Poista</string>
|
||||
<string name="translations_auto">Automaattinen</string>
|
||||
<string name="translations_translated_from">käännös kohteesta</string>
|
||||
<string name="translations_to">kohteeseen</string>
|
||||
<string name="translations_show_in_lang_first">Näytä ensin kielellä %1$s</string>
|
||||
<string name="translations_always_translate_to_lang">Aina käännä kielelle %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">Älä koskaan käännä kielestä %1$s</string>
|
||||
<string name="nip_05">Nostr-osoite</string>
|
||||
<string name="never">ei koskaan</string>
|
||||
<string name="now">nyt</string>
|
||||
<string name="h">t</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">pv</string>
|
||||
<string name="nudity">Alastomuus</string>
|
||||
<string name="profanity_hateful_speech">Loukkaava kielenkäyttö / vihapuhe</string>
|
||||
<string name="report_hateful_speech">Ilmoita vihapuheesta</string>
|
||||
<string name="report_nudity_porn">Ilmoita alastomuudesta / pornosta</string>
|
||||
<string name="others">muut</string>
|
||||
<string name="mark_all_known_as_read">Merkitse kaikki Tunnetut luetuiksi</string>
|
||||
<string name="mark_all_new_as_read">Merkitse kaikki Uudet luetuiksi</string>
|
||||
<string name="mark_all_as_read">Merkitse kaikki luetuiksi</string>
|
||||
<string name="backup_keys">Varmuuskopioi Avaimet</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Avaimen varmuuskopiointi ja turvallisuusvinkit
|
||||
\n\nTiliäsi suojaa salainen avain. Avain on pitkä satunnainen merkkijono, joka alkaa **nsec1**. Kaikki, joilla on pääsy salaiseen avaimeesi, voivat julkaista sisältöä käyttäen identiteettiäsi.
|
||||
\n\n- Älä **koskaan** laita salaista avaintasi mihinkään verkkosivustoon tai ohjelmistoon, johon et luota.
|
||||
\n- Amethyst-kehittäjät eivät **koskaan** kysy sinulta salaista avaintasi.
|
||||
\n- **Säilytä** salaisen avaimen varmuuskopio turvallisesti tilin palauttamista varten. Suosittelemme salasanahallintaohjelman käyttämistä.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Salainen avain (nsec) kopioitu leikepöydälle</string>
|
||||
<string name="copy_my_secret_key">Kopioi salainen avain</string>
|
||||
<string name="biometric_authentication_failed">Tunnistautuminen epäonnistui</string>
|
||||
<string name="biometric_error">Virhe</string>
|
||||
<string name="badge_created_by">"Luonut %1$s"</string>
|
||||
<string name="badge_award_image_for">"Merkki kohteelle %1$s"</string>
|
||||
<string name="new_badge_award_notif">Olet saanut uuden merkin</string>
|
||||
<string name="award_granted_to">Merkki myönnetty kohteelle</string>
|
||||
<string name="copied_note_text_to_clipboard">Viestin teksti kopioitu leikepöydälle</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Käyttäjän @npub kopioitu leikepöydälle</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Viestin tunniste (@note1) kopioitu leikepöydälle</string>
|
||||
<string name="select_text_dialog_top">Valitse teksti</string>
|
||||
<string name="private_conversation_notification">"<Ei voitu purkaa yksityisviestiä>\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">(0–100)%</string>
|
||||
<string name="poll_closing_time">Sulje äänestys</string>
|
||||
<string name="poll_closing_time_days">päivän kuluttua</string>
|
||||
<string name="poll_is_closed">Äänestys on suljettu uusilta ääniltä</string>
|
||||
<string name="poll_zap_amount">Zap-määrä</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Vain yksi ääni käyttäjää kohti tällä äänestystyypillä</string>
|
||||
<string name="looking_for_event">"Etsitään tapahtumaa %1$s"</string>
|
||||
<string name="custom_zaps_add_a_message">Lisää julkinen viesti</string>
|
||||
<string name="custom_zaps_add_a_message_private">Lisää yksityinen viesti</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Lisää laskuviesti</string>
|
||||
<string name="custom_zaps_add_a_message_example">Kiitos tehdystä työstä!</string>
|
||||
<string name="lightning_create_and_add_invoice">Luo ja lisää</string>
|
||||
<string name="poll_author_no_vote">Äänestyksen luojat eivät voi äänestää omissa äänestyksissään.</string>
|
||||
<string name="hash_verification_passed">Tämä sisältö on sama kuin viestin julkaisuhetkellä</string>
|
||||
<string name="hash_verification_failed">Tämä sisältö on muuttunut. Kirjoittaja ei ehkä ole nähnyt tai hyväksynyt muutosta</string>
|
||||
<string name="content_description_add_image">Lisää kuva</string>
|
||||
<string name="content_description_add_video">Lisää video</string>
|
||||
<string name="content_description_add_document">Lisää dokumentti</string>
|
||||
<string name="add_content">Lisää viestiin</string>
|
||||
<string name="content_description">Sisällön kuvaus</string>
|
||||
<string name="content_description_example">Sininen vene valkoisella hiekkarannalla auringonlaskussa</string>
|
||||
<string name="zap_type">Zap-tyyppi</string>
|
||||
<string name="zap_type_explainer">Zap-tyyppi kaikille vaihtoehdoille</string>
|
||||
<string name="zap_type_public">Julkinen</string>
|
||||
<string name="zap_type_public_explainer">Kaikki voivat nähdä tapahtuman ja viestin</string>
|
||||
<string name="zap_type_private">Yksityinen</string>
|
||||
<string name="zap_type_private_explainer">Lähettäjä ja vastaanottaja voivat nähdä toisensa ja lukea viestin</string>
|
||||
<string name="zap_type_anonymous">Anonyymi</string>
|
||||
<string name="zap_type_anonymous_explainer">Vastaanottaja ja yleisö eivät tiedä, kuka lähetti maksun</string>
|
||||
<string name="zap_type_nonzap">Ei-Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Ei jälkiä Nostrissa, vain Lightningissa</string>
|
||||
<string name="file_server">Tiedostopalvelin</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress tai @Käyttäjä</string>
|
||||
<string name="upload_server_imgur">imgur.com - luotettava</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - luotettava</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - luotettava</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - luotettava</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev voi muokata tiedostoa</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - luotettava</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">Nostrcheck.me voi muokata tiedostoa</string>
|
||||
<string name="upload_server_imgur_nip94">Todennettava Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Tarkistaa, onko Imgur muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrimg_nip94">Todennettava NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Tarkistaa, onko NostrImg muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Todennettava Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Tarkistaa, onko Nostr.build muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Todennettava Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Tarkistaa, onko Nostrfiles.dev muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Todennettava Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Tarkistaa, onko Nostrcheck.me muokannut tiedostoa. Uusi NIP: muut asiakkaat eivät ehkä näe sitä</string>
|
||||
<string name="upload_server_relays_nip95">Omat releet (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Tiedostot ovat isännöityinä omilla releilläsi. Uusi NIP: tarkista, tukevatko ne sitä</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot-asetus</string>
|
||||
<string name="connect_via_tor">Yhdistä Orbot-asetuksesi kautta</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Haluatko todella katkaista Orbot/Tor-yhteytesi?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Tietosi siirtyvät välittömästi tavalliseen verkkoon</string>
|
||||
<string name="yes">Kyllä</string>
|
||||
<string name="no">Ei</string>
|
||||
<string name="follow_list_selection">Seuraa-lista</string>
|
||||
<string name="follow_list_kind3follows">Kaikki seuraajat</string>
|
||||
<string name="follow_list_global">Maailmanlaajuinen</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Yhdistä Torin kautta Orbotin avulla
|
||||
\n\n1. Asenna [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Käynnistä Orbot
|
||||
\n3. Orbotissa tarkista Socks-portti. Oletusarvo on 9050
|
||||
\n4. Tarvittaessa vaihda porttia Orbotissa
|
||||
\n5. Aseta Socks-portti tällä näytöllä
|
||||
\n6. Paina Aktivoi-painiketta käyttääksesi Orbotia välityspalvelimena
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks-portti</string>
|
||||
<string name="invalid_port_number">Virheellinen porttinumero</string>
|
||||
<string name="use_orbot">Käytä Orbotia</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Katkaise Tor/Orbot-yhteys</string>
|
||||
<string name="app_notification_dms_channel_name">Yksityisviestit</string>
|
||||
<string name="app_notification_dms_channel_description">Ilmoittaa sinulle, kun yksityinen viesti saapuu</string>
|
||||
<string name="app_notification_zaps_channel_name">Saapuneet Zapit</string>
|
||||
<string name="app_notification_zaps_channel_description">Ilmoittaa sinulle, kun joku zappaa sinua</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">Lähettäjä: %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">saaja: %1$s</string>
|
||||
<string name="reply_notify">Ilmoitus: </string>
|
||||
<string name="channel_list_join_conversation">Liity keskusteluun</string>
|
||||
<string name="channel_list_user_or_group_id">Käyttäjän tai ryhmän tunniste</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent tai heksadesimaali</string>
|
||||
<string name="channel_list_create_channel">Luo</string>
|
||||
<string name="channel_list_join_channel">Liity</string>
|
||||
<string name="today">Tänään</string>
|
||||
<string name="content_warning">Sisältövaroitus</string>
|
||||
<string name="content_warning_explanation">Tämä viesti sisältää herkkää sisältöä, joka voi olla loukkaavaa tai häiritsevää joillekin ihmisille</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Piilota aina herkkä sisältö</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Näytä aina herkkä sisältö</string>
|
||||
<string name="content_warning_see_warnings">Näytä aina sisältövaroitukset</string>
|
||||
<string name="recommended_apps">Suosittelee: </string>
|
||||
<string name="filter_spam_from_strangers">Suodata roskapostia tuntemattomilta</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Varoita, kun seurattavasi ovat raportoineet viestejä</string>
|
||||
<string name="new_reaction_symbol">Uusi reaktiosymboli</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Ei valittu reaktiotyyppejä. Pidä pitkään painettuna vaihtaaksesi.</string>
|
||||
<string name="zapraiser">Zapkerääjä</string>
|
||||
<string name="zapraiser_explainer">Lisää tavoitesumma satseina tämän viestin tukemiseksi. Tuettavat asiakkaat voivat näyttää tämän edistymispalkkina tuen kannustamiseksi</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Tavoitesumma satseina</string>
|
||||
<string name="sats_to_complete">Zapraiser %1$s. %2$s satsiin tavoitteesta</string>
|
||||
<string name="read_from_relay">Lue releestä</string>
|
||||
<string name="write_to_relay">Kirjoita releeseen</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Virhe yritettäessä hakea rele-tietoja %1$s:stä</string>
|
||||
<string name="owner">Omistaja</string>
|
||||
<string name="version">Versio</string>
|
||||
<string name="software">Ohjelmisto</string>
|
||||
<string name="contact">Yhteystiedot</string>
|
||||
<string name="supports">Tuetut NIPit</string>
|
||||
<string name="admission_fees">Pääsymaksut</string>
|
||||
<string name="payments_url">Maksut URL</string>
|
||||
<string name="limitations">Rajoitukset</string>
|
||||
<string name="countries">Maat</string>
|
||||
<string name="languages">Kielet</string>
|
||||
<string name="tags">Tagit</string>
|
||||
<string name="posting_policy">Julkaisupolitiikka</string>
|
||||
<string name="message_length">Viestin pituus</string>
|
||||
<string name="subscriptions">Tilaukset</string>
|
||||
<string name="filters">Suodattimet</string>
|
||||
<string name="subscription_id_length">Tilaustunnuksen pituus</string>
|
||||
<string name="minimum_prefix">Vähimmäisliite</string>
|
||||
<string name="maximum_event_tags">Maksimi tapahtumien tagit</string>
|
||||
<string name="content_length">Sisällön pituus</string>
|
||||
<string name="minimum_pow">Vähimmäis PoW</string>
|
||||
<string name="auth">Autentikointi</string>
|
||||
<string name="payment">Maksu</string>
|
||||
<string name="cashu">Cashu-tunniste</string>
|
||||
<string name="cashu_redeem">Lunasta</string>
|
||||
<string name="no_lightning_address_set">Ei Lightning-osoitetta asetettuna</string>
|
||||
<string name="copied_token_to_clipboard">Kopioitu tunniste leikepöydälle</string>
|
||||
<string name="live_stream_live_tag">LIVE</string>
|
||||
<string name="live_stream_offline_tag">POISSA</string>
|
||||
<string name="live_stream_ended_tag">PÄÄTTYNYT</string>
|
||||
<string name="live_stream_planned_tag">AJOITETTU</string>
|
||||
<string name="live_stream_is_offline">Livestream on offline-tilassa</string>
|
||||
<string name="live_stream_has_ended">Livestream on päättynyt</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Uloskirjautuminen poistaa kaikki paikalliset tiedot. Varmista, että sinulla on varmuuskopio yksityisavaimistasi välttääksesi tilisi menetyksen. Haluatko jatkaa?</string>
|
||||
<string name="followed_tags">Seuratut tagit</string>
|
||||
<string name="relay_setup">Releet</string>
|
||||
<string name="discover_live">Live</string>
|
||||
<string name="discover_community">Yhteisö</string>
|
||||
<string name="discover_chat">Keskustelut</string>
|
||||
<string name="community_approved_posts">Hyväksytyt viestit</string>
|
||||
<string name="groups_no_descriptor">Tällä ryhmällä ei ole kuvausta tai sääntöjä. Keskustele omistajan kanssa sen lisäämiseksi</string>
|
||||
<string name="community_no_descriptor">Tällä yhteisöllä ei ole kuvausta. Keskustele omistajan kanssa sen lisäämiseksi</string>
|
||||
<string name="add_sensitive_content_label">Herkkä sisältö</string>
|
||||
<string name="add_sensitive_content_description">Lisää herkän sisällön varoitus ennen tämän sisällön näyttämistä</string>
|
||||
<string name="settings">Asetukset</string>
|
||||
<string name="connectivity_type_always">Aina</string>
|
||||
<string name="connectivity_type_wifi_only">Vain WiFi</string>
|
||||
<string name="connectivity_type_never">Ei koskaan</string>
|
||||
<string name="system">Järjestelmä</string>
|
||||
<string name="light">Vaalea</string>
|
||||
<string name="dark">Tumma</string>
|
||||
<string name="application_preferences">Sovelluksen asetukset</string>
|
||||
<string name="language">Kieli</string>
|
||||
<string name="theme">Teema</string>
|
||||
<string name="automatically_load_images_gifs">Kuvan esikatselu</string>
|
||||
<string name="automatically_play_videos">Videon toisto</string>
|
||||
<string name="automatically_show_url_preview">URL-esikatselu</string>
|
||||
<string name="load_image">Lataa kuva</string>
|
||||
<string name="spamming_users">Roskapostittajat</string>
|
||||
<string name="muted_button">Ääni pois päältä. Napsauta palauttaaksesi äänet</string>
|
||||
<string name="mute_button">Ääni päällä. Napsauta vaimentaaksesi</string>
|
||||
<string name="search_button">Hae paikallisia ja etämerkintöjä</string>
|
||||
<string name="nip05_verified">Nostr-osoite varmennettiin</string>
|
||||
<string name="nip05_failed">Nostr-osoite ei läpäissyt varmennusta</string>
|
||||
<string name="nip05_checking">Tarkistetaan Nostr-osoite</string>
|
||||
<string name="select_deselect_all">Valitse/Poista valinta kaikista</string>
|
||||
<string name="default_relays">Oletus</string>
|
||||
<string name="select_a_relay_to_continue">Valitse rele jatkaaksesi</string>
|
||||
<string name="zap_forward_title">Ohjaa zappit kohteeseen:</string>
|
||||
<string name="zap_forward_explainer">Sovelluksien tukeminen ohjaa zappit alla olevaan LN-osoitteeseen tai käyttäjäprofiiliin sen sijaan, että ne menisivät sinulle</string>
|
||||
<string name="geohash_title">Näytä sijaintina </string>
|
||||
<string name="geohash_explainer">Lisää sijaintisi Geohashinä viestiin. On julkisesti nähtävänä, että olet 5 km (3 mailin) säteellä nykyisestä sijainnista</string>
|
||||
<string name="add_sensitive_content_explainer">Lisää herkkä sisältö -varoitus ennen sisällön näyttämistä. Tämä on suositeltua kaikelle aikuissisällölle tai sisällölle, jota jotkut saattavat pitää loukkaavana tai häiritsevänä</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Uusi ominaisuus</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Tämän tilan aktivoiminen vaatii Amethystin lähettämään NIP-24-viestin (Lahjapakatut, Suljetut suorat ja ryhmäviestit). NIP-24 on uusi, ja useimmat asiakkaat eivät ole vielä toteuttaneet sitä. Varmista, että vastaanottaja käyttää yhteensopivaa asiakasta.</string>
|
||||
<string name="new_feature_nip24_activate">Aktivoi</string>
|
||||
<string name="messages_create_public_chat">Julkinen</string>
|
||||
<string name="messages_new_message">Yksityinen</string>
|
||||
<string name="messages_new_message_to">Vastaanottaja</string>
|
||||
<string name="messages_new_message_subject">Aihe</string>
|
||||
<string name="messages_new_message_subject_caption">Keskustelun aihe</string>
|
||||
<string name="messages_new_message_to_caption">"\@Käyttäjä1, @Käyttäjä2, @Käyttäjä3"</string>
|
||||
<string name="messages_group_descriptor">Tämän ryhmän jäsenet</string>
|
||||
<string name="messages_new_subject_message">Selitys jäsenille</string>
|
||||
<string name="messages_new_subject_message_placeholder">Muuta nimi uusille tavoitteille.</string>
|
||||
<string name="language_description">Sovelluksen käyttöliittymälle</string>
|
||||
<string name="theme_description">Tumma, vaalea tai järjestelmäteema</string>
|
||||
<string name="automatically_load_images_gifs_description">Lataa kuvat ja GIF-animaatiot automaattisesti</string>
|
||||
<string name="automatically_play_videos_description">Toista videot ja GIF-animaatiot automaattisesti</string>
|
||||
<string name="automatically_show_url_preview_description">Näytä URL-esikatselut</string>
|
||||
<string name="load_image_description">Koska kuvat ladataan</string>
|
||||
<string name="copy_url_to_clipboard">Kopioi URL leikepöydälle</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Kopioi viestin tunniste leikepöydälle</string>
|
||||
<string name="created_at">Luotu</string>
|
||||
<string name="rules">Säännöt</string>
|
||||
<string name="status_update">Päivitä tilaasi</string>
|
||||
<string name="lightning_wallets_not_found">Virhe tulkittaessa virhesanomaa</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Äänet painotetaan zappin määrän perusteella. Voit asettaa minimimäärän roskapostin välttämiseksi ja maksimimäärän suurten zappereiden estämiseksi. Käytä samaa määrää molemmissa kentissä, jotta jokainen ääni arvostetaan samalla määrällä. Jätä se tyhjäksi hyväksyäksesi minkä tahansa määrän.</string>
|
||||
<string name="error_dialog_zap_error">Zapin lähettäminen ei onnistunut</string>
|
||||
<string name="error_dialog_talk_to_user">Viesti käyttäjälle</string>
|
||||
<string name="error_dialog_button_ok">Ok</string>
|
||||
<string name="relay_information_document_error_assemble_url">Kohdetta %1$s ei tavoitettu: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Kohdetta %1$s ei tavoitettu: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Tuloksen jäsentäminen epäonnistui %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s epäonnistui koodilla %2$s</string>
|
||||
<string name="active_for">Aktiivinen kohteelle: </string>
|
||||
<string name="active_for_home">Koti</string>
|
||||
<string name="active_for_msg">YVt</string>
|
||||
<string name="active_for_chats">Keskustelut</string>
|
||||
<string name="active_for_global">Yleinen</string>
|
||||
<string name="active_for_search">Haku</string>
|
||||
</resources>
|
||||
2
app/src/main/res/values-fo-rFO/strings.xml
Normal file
2
app/src/main/res/values-fo-rFO/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
2
app/src/main/res/values-fr-rCA/strings.xml
Normal file
2
app/src/main/res/values-fr-rCA/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
@@ -5,11 +5,11 @@
|
||||
<string name="profile_image">Image de profil</string>
|
||||
<string name="scan_qr">Scanner le QR code</string>
|
||||
<string name="show_anyway">Montrer quand même</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post signalé innaproprié par</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post signalé inapproprié par</string>
|
||||
<string name="post_not_found">Post non trouvé</string>
|
||||
<string name="channel_image">Image du canal</string>
|
||||
<string name="referenced_event_not_found">référence de l\'évènement non trouvée</string>
|
||||
<string name="could_not_decrypt_the_message">Impossibe de déchiffrer le message</string>
|
||||
<string name="could_not_decrypt_the_message">Impossible de déchiffrer le message</string>
|
||||
<string name="group_picture">Image de groupe</string>
|
||||
<string name="explicit_content">Contenu choquant</string>
|
||||
<string name="spam">Spam</string>
|
||||
@@ -22,8 +22,10 @@
|
||||
<string name="copy_user_pubkey">Copier la clé publique de l\'utilisateur</string>
|
||||
<string name="copy_note_id">Copier l\'ID de la note</string>
|
||||
<string name="broadcast">Diffuser</string>
|
||||
<string name="block_hide_user">Bloquer et cacher l\'utilisateur</string>
|
||||
<string name="report_spam_scam">Signaler un Spam / Scam</string>
|
||||
<string name="request_deletion">Demande de suppression</string>
|
||||
<string name="block_report">Bloquer / Signaler</string>
|
||||
<string name="block_hide_user"><![CDATA[Bloquer et cacher l\'utilisateur]]></string>
|
||||
<string name="report_spam_scam">Signaler Spam / Arnaque</string>
|
||||
<string name="report_impersonation">Signaler une falsification d\'identité</string>
|
||||
<string name="report_explicit_content">Signaler un contenu choquant</string>
|
||||
<string name="report_illegal_behaviour">Signaler un comportement illégal</string>
|
||||
@@ -32,6 +34,8 @@
|
||||
<string name="login_with_a_private_key_to_like_posts">Connectez vous avec une clé privée pour pouvoir aimer des posts</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Aucun montant sélectionné pour le zap. Appui long pour modifier</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Connectez vous avec une clé privée pour pouvoir envoyer des zaps</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">Connectez-vous avec une clé privée pour Suivre</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Connectez-vous avec une clé privée pour ne plus Suivre</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Nombre de vues</string>
|
||||
<string name="boost">Boost</string>
|
||||
@@ -43,9 +47,11 @@
|
||||
<string name="and">" et "</string>
|
||||
<string name="in_channel">"dans le canal "</string>
|
||||
<string name="profile_banner">Bannière du profil</string>
|
||||
<string name="payment_successful">Paiement Réussi</string>
|
||||
<string name="error_parsing_error_message">Erreur d\'analyse du message d\'erreur</string>
|
||||
<string name="following">" Abonnements"</string>
|
||||
<string name="followers">" Abonnés"</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="profile">Profil</string>
|
||||
<string name="security_filters">Filtres de sécurité</string>
|
||||
<string name="log_out">Déconnexion</string>
|
||||
<string name="show_more">Voir plus</string>
|
||||
@@ -53,17 +59,17 @@
|
||||
<string name="pay">Payer</string>
|
||||
<string name="lightning_tips">Pourboire Lightning</string>
|
||||
<string name="note_to_receiver">Note au destinataire</string>
|
||||
<string name="thank_you_so_much">Merci beaucoup!</string>
|
||||
<string name="thank_you_so_much">Merci beaucoup !</string>
|
||||
<string name="amount_in_sats">Montant en Sats</string>
|
||||
<string name="send_sats">Envoyer des Sats</string>
|
||||
<string name="error_parsing_preview_for">"Erreur d\'analyse de l\'aperçu pour %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Aperçu de l\'image pour %1$s"</string>
|
||||
<string name="error_parsing_preview_for">"Erreur d'analyse de l'aperçu pour %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Aperçu de l'image pour %1$s"</string>
|
||||
<string name="new_channel">Nouveau canal</string>
|
||||
<string name="channel_name">Nom de canal</string>
|
||||
<string name="my_awesome_group">Mon groupe spectaculaire</string>
|
||||
<string name="picture_url">Url de l\'image</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="about_us">à propos</string>
|
||||
<string name="about_us">"à propos"</string>
|
||||
<string name="what_s_on_your_mind">Quoi de neuf ?</string>
|
||||
<string name="post">Envoyer</string>
|
||||
<string name="save">Sauvegarder</string>
|
||||
@@ -72,6 +78,7 @@
|
||||
<string name="failed_to_upload_the_image">Échec du téléchargement de l\'image</string>
|
||||
<string name="relay_address">Adresse du relais</string>
|
||||
<string name="posts">Publications</string>
|
||||
<string name="bytes">Octets</string>
|
||||
<string name="errors">Erreurs</string>
|
||||
<string name="home_feed">Vos notifications</string>
|
||||
<string name="private_message_feed">Flux de messages privés</string>
|
||||
@@ -94,7 +101,7 @@
|
||||
<string name="upload_image">Charger une image</string>
|
||||
<string name="uploading">Chargement…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
|
||||
<string name="reply_here">"Répondre ici.. "</string>
|
||||
<string name="reply_here">"répondre ici"</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Copier l\'ID de la chaîne (Note) dans le presse-papiers</string>
|
||||
<string name="edits_the_channel_metadata">Modifie les métadonnées du canal</string>
|
||||
@@ -119,17 +126,18 @@
|
||||
<string name="send_a_direct_message">Envoyer un message direct</string>
|
||||
<string name="edits_the_user_s_metadata">Modifie les métadonnées de l\'utilisateur</string>
|
||||
<string name="follow">Suivre</string>
|
||||
<string name="follow_back">Suivre en retour</string>
|
||||
<string name="unblock">Débloquer</string>
|
||||
<string name="copy_user_id">Copier l\'ID utilisateur</string>
|
||||
<string name="unblock_user">Débloquer l\'utilisateur</string>
|
||||
<string name="npub_hex_username">"npub, hex, nom d\'utilisateur "</string>
|
||||
<string name="npub_hex_username">"npub, hex, nom d'utilisateur "</string>
|
||||
<string name="clear">Nettoyer</string>
|
||||
<string name="app_logo">Logo de l\'App</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string>
|
||||
<string name="show_password">Révéler le mot de passe</string>
|
||||
<string name="hide_password">Cacher le mot de passe</string>
|
||||
<string name="invalid_key">Clé non valide</string>
|
||||
<string name="i_accept_the">"J\'accepte les "</string>
|
||||
<string name="i_accept_the">"J'accepte les "</string>
|
||||
<string name="terms_of_use">conditions d\'utilisation</string>
|
||||
<string name="acceptance_of_terms_is_required">L\'acceptation des conditions est requise</string>
|
||||
<string name="key_is_required">La clé est requise</string>
|
||||
@@ -137,7 +145,7 @@
|
||||
<string name="generate_a_new_key">Générer une nouvelle clé</string>
|
||||
<string name="loading_feed">Chargement du flux</string>
|
||||
<string name="error_loading_replies">"Erreur lors du chargement des réponses : "</string>
|
||||
<string name="try_again">essayer à nouveau</string>
|
||||
<string name="try_again">Essayer à nouveau</string>
|
||||
<string name="feed_is_empty">Le flux est vide.</string>
|
||||
<string name="refresh">Rafraîchir</string>
|
||||
<string name="created">créé</string>
|
||||
@@ -159,6 +167,7 @@
|
||||
<string name="translations_show_in_lang_first">Montre en %1$s en premier</string>
|
||||
<string name="translations_always_translate_to_lang">Toujours traduire en %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">Ne jamais traduire depuis %1$s</string>
|
||||
<string name="nip_05">Adresse Nostr</string>
|
||||
<string name="never">jamais</string>
|
||||
<string name="now">maintenant</string>
|
||||
<string name="h">h</string>
|
||||
@@ -173,8 +182,7 @@
|
||||
<string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string>
|
||||
<string name="mark_all_as_read">Tout marquer comme lu</string>
|
||||
<string name="backup_keys">Clés de sauvegarde</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## Sauvegarde des clés et conseils de sécurité
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Sauvegarde des clés et conseils de sécurité
|
||||
\n\nVotre compte est sécurisé par une clé secrète. La clé est une longue chaîne aléatoire commençant par **nsec1**. Toute personne ayant accès à votre clé secrète peut publier du contenu en utilisant votre identité.
|
||||
\n\n- Ne communiquez **jamais** votre clé secrète à un site Web ou un logiciel auquel vous ne faites pas confiance.
|
||||
\n- Les développeurs d\'Amethyst ne vous demanderont **jamais** votre clé secrète.
|
||||
@@ -192,7 +200,6 @@
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copie du @npub utilisateur dans le presse-papiers</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Copie de l\'ID de la note (@note1) dans le presse-papiers</string>
|
||||
<string name="select_text_dialog_top">Sélectionner le Texte</string>
|
||||
|
||||
<string name="private_conversation_notification">"<Impossible de déchiffrer le message privé>\n\nVous avez été cité dans une conversation privée/cryptée entre %1$s et %2$s."</string>
|
||||
<string name="account_switch_add_account_dialog_title">Ajouter un Nouveau Compte</string>
|
||||
<string name="drawer_accounts">Comptes</string>
|
||||
@@ -235,8 +242,6 @@
|
||||
<string name="report_dialog_post_report_btn">Publier le signalement</string>
|
||||
<string name="report_dialog_title">Bloquer et Signaler</string>
|
||||
<string name="block_only">Bloquer</string>
|
||||
|
||||
|
||||
<string name="bookmarks">Favoris</string>
|
||||
<string name="private_bookmarks">Favoris Privés</string>
|
||||
<string name="public_bookmarks">Favoris Publics</string>
|
||||
@@ -244,15 +249,13 @@
|
||||
<string name="add_to_public_bookmarks">Ajouter aux Favoris Publics</string>
|
||||
<string name="remove_from_private_bookmarks">Supprimer des Favoris Privés</string>
|
||||
<string name="remove_from_public_bookmarks">Supprimer des Favoris Publics</string>
|
||||
|
||||
<string name="wallet_connect_service">Service Wallet Connect</string>
|
||||
<string name="wallet_connect_service_explainer">Autorise un Nostr Secret à payer des zaps sans quitter l\'application. Gardez le secret en toute sécurité et utilisez un relais privé si possible</string>
|
||||
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
|
||||
<string name="wallet_connect_service_pubkey">Clé publique Wallet Connect</string>
|
||||
<string name="wallet_connect_service_relay">Relais Wallet Connect</string>
|
||||
<string name="wallet_connect_service_secret">Secret Wallet Connect</string>
|
||||
<string name="wallet_connect_service_show_secret">Montrer la clé secrète</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">Clé privée nsec / hex</string>
|
||||
|
||||
<string name="pledge_amount_in_sats">Montant engagé en Sats</string>
|
||||
<string name="post_poll">Message Sondage</string>
|
||||
<string name="poll_heading_required">Champs requis:</string>
|
||||
@@ -260,7 +263,7 @@
|
||||
<string name="poll_primary_description">Description du sondage primaire…</string>
|
||||
<string name="poll_option_index">Option %s</string>
|
||||
<string name="poll_option_description">Description de l\'option du Sondage</string>
|
||||
<string name="poll_heading_optional">Champs optionels:</string>
|
||||
<string name="poll_heading_optional">Champs optionnels :</string>
|
||||
<string name="poll_zap_value_min">Zap minimum</string>
|
||||
<string name="poll_zap_value_max">Zap maximum</string>
|
||||
<string name="poll_consensus_threshold">Consensus</string>
|
||||
@@ -270,97 +273,65 @@
|
||||
<string name="poll_is_closed">Le Sondage est fermé aux nouveaux votes</string>
|
||||
<string name="poll_zap_amount">Montant des Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Un seul vote par utilisateur est autorisé sur ce type de sondage</string>
|
||||
|
||||
<string name="looking_for_event">"Recherche de l'Evénement %1$s"</string>
|
||||
|
||||
<string name="looking_for_event">"Recherche de l'Événement %1$s"</string>
|
||||
<string name="custom_zaps_add_a_message">Ajouter un message public</string>
|
||||
<string name="custom_zaps_add_a_message_private">Ajouter un message privé</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Ajouter un message facture</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">Merci pour tout votre travail !</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">Créer et Ajouter</string>
|
||||
<string name="poll_author_no_vote">Les auteurs du sondage ne peuvent pas voter dans leurs propres sondages.</string>
|
||||
|
||||
<string name="hash_verification_passed">Ce contenu est le même depuis le message</string>
|
||||
<string name="hash_verification_failed">Ce contenu a changé. L\'auteur n\'a peut-être pas vu ou approuvé le changement</string>
|
||||
|
||||
<string name="content_description_add_image">Ajouter une Image</string>
|
||||
<string name="content_description_add_video">Ajouter une Vidéo</string>
|
||||
<string name="content_description_add_document">Ajouter un Document</string>
|
||||
|
||||
<string name="add_content">Ajouter au Message</string>
|
||||
<string name="content_description">Description des contenus</string>
|
||||
<string name="content_description_example">Un bateau bleu sur une plage de sable blanc au coucher du soleil</string>
|
||||
|
||||
|
||||
<string name="zap_type">Type de Zap</string>
|
||||
<string name="zap_type_explainer">Zap Type pour toutes les options</string>
|
||||
|
||||
<string name="zap_type_public">Public</string>
|
||||
<string name="zap_type_public_explainer">Tout le monde peut voir la transaction et le message</string>
|
||||
|
||||
<string name="zap_type_private">Privé</string>
|
||||
<string name="zap_type_private_explainer">L\'expéditeur et le receveur peuvent se voir et lire le message</string>
|
||||
|
||||
<string name="zap_type_anonymous">Anonyme</string>
|
||||
<string name="zap_type_anonymous_explainer">Le receveur et le public ne savent pas qui a envoyé le paiement</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Aucune trace sur Nostr, seulement sur le Lightning</string>
|
||||
|
||||
|
||||
<string name="file_server">Serveur fichier</string>
|
||||
<string name="zap_forward_lnAddress">Adresse LN ou @Utilisateur</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - de confiance</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - de confiance</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - de confiance</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - de confiance</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - de confiance</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me peut modifier le fichier</string>
|
||||
|
||||
|
||||
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Vérifie si Imgur a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Vérifie si NostrImg a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Vérifie si Nostr.build a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifiable Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Vérifie si Nostrfiles.dev a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Nostrcheck.me vérifiable (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Vérifie si Nostrcheck.me a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
|
||||
|
||||
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Se connecter à travers la configuration Orbot</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Se déconnecter de Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Vos données seront immédiatement transférées sur le réseau classique</string>
|
||||
<string name="yes">Oui</string>
|
||||
<string name="no">Non</string>
|
||||
|
||||
|
||||
<string name="follow_list_selection">Liste des Abonnés</string>
|
||||
<string name="follow_list_kind3follows">Tous les abonnements</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Se connecter à travers Tor avec Orbot
|
||||
<string name="follow_list_global">Général</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Se connecter à travers Tor avec Orbot
|
||||
\n\n1. Installe [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Démarre Orbot
|
||||
\n3. Dans Orbot, vérifie le port Socks. Par défaut 9050
|
||||
@@ -372,44 +343,33 @@
|
||||
<string name="invalid_port_number">Numéro de port invalide</string>
|
||||
<string name="use_orbot">Utiliser Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Déconnecter Tor/Orbot</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">Messages privés</string>
|
||||
<string name="app_notification_dms_channel_description">Vous notifie lorsqu\'un message privé est reçu</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">Zaps reçus</string>
|
||||
<string name="app_notification_zaps_channel_description">Vous notifie lorsque quelqu\'un vous zap</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">pour %1$s</string>
|
||||
|
||||
|
||||
<string name="reply_notify">Notifier: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Rejoindre Conversation</string>
|
||||
<string name="channel_list_user_or_group_id">Utilisateur ou Groupe ID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent ou hex</string>
|
||||
<string name="channel_list_create_channel">Créer</string>
|
||||
<string name="channel_list_join_channel">Rejoindre</string>
|
||||
|
||||
<string name="today">Aujourd\'hui</string>
|
||||
|
||||
<string name="content_warning">Avertissement de contenu</string>
|
||||
<string name="content_warning_explanation">Ce message contient du contenu sensible que certaines personnes peuvent trouver offensant ou dérangeant</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Toujours cacher les contenus sensibles</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Toujours afficher les contenus sensibles</string>
|
||||
<string name="content_warning_see_warnings">Toujours afficher les avertissements de contenu</string>
|
||||
|
||||
<string name="recommended_apps">Recommendations: </string>
|
||||
<string name="filter_spam_from_strangers">Filtrer le spam des étrangers</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avertir lorsque les messages ont été rapportés par vos abonnés</string>
|
||||
|
||||
<string name="new_reaction_symbol">Nouveau symbole de réaction</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Aucun type de réaction sélectionné. Appuyer longuement pour changer</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser">Collecte de Zaps</string>
|
||||
<string name="zapraiser_explainer">Ajoute une quantité cible de sats pour ce message. Les clients compatible peuvent afficher une barre de progrès pour encourager les dons</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Quantité ciblée en Sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser à %1$s. %2$s sats pour objectif</string>
|
||||
<string name="read_from_relay">Lire depuis le Relay</string>
|
||||
<string name="write_to_relay">Écrire sur le Relay</string>
|
||||
@@ -421,10 +381,10 @@
|
||||
<string name="supports">NIPs supportés</string>
|
||||
<string name="admission_fees">Frais d\'admission</string>
|
||||
<string name="payments_url">URL de payments</string>
|
||||
<string name="limitations">Limitations</string>
|
||||
<string name="limitations">Limites</string>
|
||||
<string name="countries">Pays</string>
|
||||
<string name="languages">Langages</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="tags">Étiquettes</string>
|
||||
<string name="posting_policy">Politique de publication</string>
|
||||
<string name="message_length">Longueur du message</string>
|
||||
<string name="subscriptions">Abonnements</string>
|
||||
@@ -440,34 +400,27 @@
|
||||
<string name="cashu_redeem">Échanger</string>
|
||||
<string name="no_lightning_address_set">Adresse Lightning non définie</string>
|
||||
<string name="copied_token_to_clipboard">Jeton copié dans le presse-papiers</string>
|
||||
|
||||
<string name="live_stream_live_tag">EN DIRECT</string>
|
||||
<string name="live_stream_offline_tag">DÉCONNECTÉ</string>
|
||||
<string name="live_stream_ended_tag">TERMINÉ</string>
|
||||
<string name="live_stream_planned_tag">PLANIFIÉ</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream est déconnecté</string>
|
||||
<string name="live_stream_has_ended">Livestream terminé</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ?</string>
|
||||
<string name="followed_tags">Tags suivis</string>
|
||||
|
||||
<string name="relay_setup">Relays</string>
|
||||
|
||||
<string name="relay_setup">Relais</string>
|
||||
<string name="discover_live">Direct</string>
|
||||
<string name="discover_community">Communauté</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="discover_chat">Salons</string>
|
||||
<string name="community_approved_posts">Messages approuvés</string>
|
||||
|
||||
<string name="groups_no_descriptor">Ce groupe n\'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une</string>
|
||||
<string name="community_no_descriptor">Cette communauté n\'a pas de description. Parlez au propriétaire pour en ajouter une</string>
|
||||
|
||||
<string name="add_sensitive_content_label">Contenu sensible</string>
|
||||
<string name="add_sensitive_content_description">Ajouter un avertissement de contenu sensible avant de montrer ce contenu</string>
|
||||
<string name="settings">Paramètres</string>
|
||||
<string name="connectivity_type_always">Toujours</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi uniquement</string>
|
||||
<string name="connectivity_type_never">Jamais</string>
|
||||
|
||||
<string name="system">Système</string>
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
@@ -478,62 +431,69 @@
|
||||
<string name="automatically_play_videos">Lecture vidéo</string>
|
||||
<string name="automatically_show_url_preview">Prévisualisation des URLs</string>
|
||||
<string name="load_image">Charger l\'image</string>
|
||||
|
||||
<string name="spamming_users">Spammeurs</string>
|
||||
|
||||
<string name="muted_button">Silencieux. Cliquer pour réactiver le son</string>
|
||||
<string name="mute_button">Son activé. Cliquer pour mettre en silencieux</string>
|
||||
<string name="search_button">Recherche d\'enregistrements locaux et distants</string>
|
||||
|
||||
<string name="nip05_verified">L\'adresse Nostr a été vérfiée</string>
|
||||
<string name="nip05_failed">La vérification de l\'adresse Nostr a échouée</string>
|
||||
<string name="nip05_checking">Vérification de l\'adresse Nostr</string>
|
||||
<string name="select_deselect_all">Tout sélectionner/désélectionner</string>
|
||||
<string name="default_relays">Défaut</string>
|
||||
<string name="select_a_relay_to_continue">Sélectionner un relay pour continuer</string>
|
||||
|
||||
<string name="zap_forward_title">Transférer les Zaps à:</string>
|
||||
<string name="zap_forward_explainer">Les clients compatibles transmettront des zaps sur l\'adresse LN ou le profil Utilisateur ci-dessous au lieu du vôtre.</string>
|
||||
|
||||
<string name="geohash_title">Révéler la localisation comme </string>
|
||||
<string name="geohash_explainer">Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Nouvelle Fonctionnalité</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Pour activer ce mode, Amethyst doit envoyer un message NIP-24 (GiftWrapped, Sealed Direct et Group Messages). Le protocole NIP-24 est nouveau et la plupart des clients ne l\'ont pas encore mis en oeuvre. Assurez-vous que le destinataire utilise un client compatible.</string>
|
||||
<string name="new_feature_nip24_activate">Activer</string>
|
||||
|
||||
<string name="messages_create_public_chat">Public</string>
|
||||
<string name="messages_new_message">Privé</string>
|
||||
<string name="messages_new_message_to">À</string>
|
||||
<string name="messages_new_message_subject">Sujet</string>
|
||||
<string name="messages_new_message_subject_caption">Sujet de la conversation</string>
|
||||
<string name="messages_new_message_to_caption">"@Utilisateur1, @Utilisateur2, @Utilisateur3"</string>
|
||||
|
||||
<string name="messages_new_message_to_caption">"\@Utilisateur1, @Utilisateur2, @Utilisateur3"</string>
|
||||
<string name="messages_group_descriptor">Membres de ce groupe</string>
|
||||
<string name="messages_new_subject_message">Explication aux membres</string>
|
||||
<string name="messages_new_subject_message_placeholder">Changement de nom pour les nouveaux objectifs.</string>
|
||||
|
||||
<string name="language_description">Pour l\'interface de l\'App</string>
|
||||
<string name="theme_description">Sombre, Clair ou thème Système</string>
|
||||
<string name="automatically_load_images_gifs_description">Charger automatiquement les images et les GIFs</string>
|
||||
<string name="automatically_play_videos_description">Lire automatiquement les vidéos et les GIFs</string>
|
||||
<string name="automatically_show_url_preview_description">Afficher la prévisualisation d\'URL</string>
|
||||
<string name="load_image_description">Quand charger les images</string>
|
||||
|
||||
<string name="copy_url_to_clipboard">Copier l\'URL dans le presse-papiers</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copier l\'ID de la note dans le presse-papiers</string>
|
||||
|
||||
<string name="created_at">Créé le</string>
|
||||
<string name="rules">Règles</string>
|
||||
|
||||
<string name="status_update">Mettez à jour votre statut</string>
|
||||
|
||||
<string name="lightning_wallets_not_found">Erreur d\'analyse du message d\'erreur</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Les votes sont pondérés par le montant du zap. Vous pouvez définir un montant minimum pour éviter les spammeurs et un montant maximum pour éviter qu\'un grand nombre de zappeurs ne prenne le contrôle du sondage. Utilisez le même montant dans les deux champs pour vous assurer que chaque vote a la même valeur. Laissez le champ vide pour accepter n\'importe quel montant.</string>
|
||||
|
||||
<string name="error_dialog_zap_error">Impossible d\'envoyer un zap</string>
|
||||
<string name="error_dialog_talk_to_user">Contacter l\'Utilisateur</string>
|
||||
<string name="error_dialog_button_ok">Ok</string>
|
||||
<string name="relay_information_document_error_assemble_url">Impossible d\'atteindre %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Impossible d\'atteindre %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Impossible d\'analyser le résultat de %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_http_status">%1$s a échoué avec le code %2$s</string>
|
||||
<string name="active_for">Actif pour : </string>
|
||||
<string name="active_for_home">Accueil</string>
|
||||
<string name="active_for_msg">MPs</string>
|
||||
<string name="active_for_chats">Salons</string>
|
||||
<string name="active_for_global">Général</string>
|
||||
<string name="active_for_search">Rechercher</string>
|
||||
<string name="zap_split_title">Diviser et Transférer des Zaps</string>
|
||||
<string name="zap_split_explainer">Les clients supportés vont diviser et transférer les zaps aux utilisateurs ajoutés ici au lieu du vôtre</string>
|
||||
<string name="zap_split_serarch_and_add_user">Rechercher et Ajouter un Utilisateur</string>
|
||||
<string name="zap_split_serarch_and_add_user_placeholder">Nom d\'utilisateur ou nom affiché</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur %1$s n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
|
||||
<string name="zap_split_weight">Pourcentage</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Diviser les zaps avec</string>
|
||||
<string name="forwarding_zaps_to">Transférer les zaps à</string>
|
||||
<string name="lightning_wallets_not_found2">Portefeuilles Lightning introuvables</string>
|
||||
<string name="paid">Payé</string>
|
||||
<string name="wallet_number">Portefeuille %1$s</string>
|
||||
</resources>
|
||||
|
||||
2
app/src/main/res/values-gu-rIN/strings.xml
Normal file
2
app/src/main/res/values-gu-rIN/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user