mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 15:44:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
133d7d5ce0 | ||
|
|
215989108b | ||
|
|
ed7dcf94f8 | ||
|
|
b2c7f150ec | ||
|
|
c85d74a688 | ||
|
|
cbf23fedf1 | ||
|
|
5ed64bd5b5 | ||
|
|
b79f9c8395 | ||
|
|
02da606461 | ||
|
|
7074446cae | ||
|
|
4746b5b18a | ||
|
|
103a61f35c | ||
|
|
d9c48d4039 | ||
|
|
9479e86b66 | ||
|
|
bb7ec1ee4b | ||
|
|
7a213545a2 | ||
|
|
48f27f656d | ||
|
|
47176421e0 | ||
|
|
9c84a538d6 | ||
|
|
8bc1c2ec68 | ||
|
|
b94c1e2792 | ||
|
|
74f0393706 | ||
|
|
0ec4b9b59f | ||
|
|
1592d55db5 | ||
|
|
629c8b8167 | ||
|
|
9d8f70e04b | ||
|
|
f75104e616 | ||
|
|
c9d59fd5ac | ||
|
|
d41eb4ce31 | ||
|
|
60e87d943a | ||
|
|
1deed59d5b | ||
|
|
bbc4ec2625 | ||
|
|
5109b65e37 | ||
|
|
c0a9ce8e21 | ||
|
|
bdff8c6c0d | ||
|
|
a3ac8a445d | ||
|
|
84d4d68116 | ||
|
|
27248bff17 | ||
|
|
dc9a9f288b | ||
|
|
88662a4e55 | ||
|
|
9d269f7cba | ||
|
|
04fe439e0b | ||
|
|
72503b9061 | ||
|
|
f7ab963db9 | ||
|
|
f97128e11c | ||
|
|
e2d4107297 | ||
|
|
18709a1b22 | ||
|
|
ddfe2abb42 | ||
|
|
0cf82f7edd | ||
|
|
d1c40efd90 | ||
|
|
68e90ce3ed | ||
|
|
95e19cf681 | ||
|
|
3298fd4493 | ||
|
|
248bdffd50 | ||
|
|
cce7a20918 | ||
|
|
845c99b798 | ||
|
|
b13b832696 | ||
|
|
eb6e235f1b | ||
|
|
36c49b2171 | ||
|
|
5cd3096221 | ||
|
|
5bdc59d0e3 |
@@ -59,7 +59,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [ ] Nostr Connect (NIP-46)
|
||||
- [x] Wallet Connect API (NIP-47)
|
||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||
- [ ] Private key encryption for import/export (NIP-49)
|
||||
- [x] Private key encryption for import/export (NIP-49)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
- [x] Lists (NIP-51)
|
||||
- [ ] Calendar Events (NIP-52)
|
||||
@@ -176,6 +176,10 @@ Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-
|
||||
|
||||
You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac)
|
||||
|
||||
By contributing to this repository, you agree to license your work under the MIT license. Any work
|
||||
contributed where you are not the original author must contain its license header with the original
|
||||
author(s) and source.
|
||||
|
||||
## How to Deploy
|
||||
|
||||
1. Generate a new signing key
|
||||
|
||||
+7
-5
@@ -12,8 +12,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 351
|
||||
versionName "0.83.10"
|
||||
versionCode 357
|
||||
versionName "0.84.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -149,6 +149,7 @@ android {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
exclude '**/libscrypt.dylib'
|
||||
}
|
||||
|
||||
lint {
|
||||
@@ -162,6 +163,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':quartz')
|
||||
implementation project(path: ':commons')
|
||||
implementation "androidx.core:core-ktx:$core_ktx_version"
|
||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||
@@ -192,7 +194,7 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// Zoomable images
|
||||
implementation 'net.engawapg.lib:zoomable:1.5.3'
|
||||
implementation 'net.engawapg.lib:zoomable:1.6.0'
|
||||
|
||||
// Biometrics
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
@@ -229,7 +231,7 @@ dependencies {
|
||||
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
||||
|
||||
// For QR generation
|
||||
implementation 'com.google.zxing:core:3.5.2'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
|
||||
// Markdown
|
||||
@@ -252,7 +254,7 @@ dependencies {
|
||||
playImplementation 'com.google.mlkit:translate:17.0.2'
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.1')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.2')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
//PushNotifications(FDroid)
|
||||
|
||||
Vendored
+5
@@ -33,6 +33,11 @@
|
||||
# JNA For Libsodium
|
||||
-keep class com.goterl.lazysodium.** { *; }
|
||||
|
||||
# libscrypt
|
||||
-keep class com.lambdaworks.codec.** { *; }
|
||||
-keep class com.lambdaworks.crypto.** { *; }
|
||||
-keep class com.lambdaworks.jni.** { *; }
|
||||
|
||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
||||
-keep class com.sun.jna.NativeMapped { *; }
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||
@@ -87,6 +88,10 @@ class Amethyst : Application() {
|
||||
return ImageLoader.Builder(applicationContext).diskCache { imageCache }
|
||||
}
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences {
|
||||
return EncryptedStorage.preferences(instance, npub)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Amethyst
|
||||
private set
|
||||
|
||||
@@ -20,32 +20,35 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
object EncryptedStorage {
|
||||
private const val PREFERENCES_NAME = "secret_keeper"
|
||||
class EncryptedStorage {
|
||||
companion object {
|
||||
private const val PREFERENCES_NAME = "secret_keeper"
|
||||
|
||||
// returns the preferences for each account or a global file if null.
|
||||
fun prefsFileName(npub: String? = null): String {
|
||||
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||
}
|
||||
// returns the preferences for each account or a global file if null.
|
||||
fun prefsFileName(npub: String? = null): String {
|
||||
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||
}
|
||||
|
||||
fun preferences(npub: String? = null): EncryptedSharedPreferences {
|
||||
val context = Amethyst.instance
|
||||
val masterKey: MasterKey =
|
||||
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
fun preferences(
|
||||
applicationContext: Context,
|
||||
npub: String? = null,
|
||||
): EncryptedSharedPreferences {
|
||||
val masterKey: MasterKey =
|
||||
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
val preferencesName = prefsFileName(npub)
|
||||
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
preferencesName,
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
) as EncryptedSharedPreferences
|
||||
return EncryptedSharedPreferences.create(
|
||||
applicationContext,
|
||||
prefsFileName(npub),
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
) as EncryptedSharedPreferences
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ object LocalPreferences {
|
||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
} else {
|
||||
return EncryptedStorage.preferences(npub)
|
||||
return Amethyst.instance.encryptedStorage(npub)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ fun checkForHashtagWithIcon(
|
||||
primary: Color,
|
||||
): HashtagIcon? {
|
||||
return when (tag.lowercase()) {
|
||||
"₿itcoin",
|
||||
"bitcoin",
|
||||
"btc",
|
||||
"timechain",
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
||||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelHideMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMuteUserEvent
|
||||
@@ -57,6 +58,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
@@ -463,6 +465,20 @@ object LocalCache {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: CommunityListEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: ChannelListEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: FileServersEvent,
|
||||
relay: Relay?,
|
||||
@@ -1772,6 +1788,7 @@ object LocalCache {
|
||||
is CalendarTimeSlotEvent -> consume(event, relay)
|
||||
is CalendarRSVPEvent -> consume(event, relay)
|
||||
is ChannelCreateEvent -> consume(event)
|
||||
is ChannelListEvent -> consume(event, relay)
|
||||
is ChannelHideMessageEvent -> consume(event)
|
||||
is ChannelMessageEvent -> consume(event, relay)
|
||||
is ChannelMetadataEvent -> consume(event)
|
||||
@@ -1779,6 +1796,7 @@ object LocalCache {
|
||||
is ChatMessageEvent -> consume(event, relay)
|
||||
is ClassifiedsEvent -> consume(event, relay)
|
||||
is CommunityDefinitionEvent -> consume(event, relay)
|
||||
is CommunityListEvent -> consume(event, relay)
|
||||
is CommunityPostApprovalEvent -> {
|
||||
event.containedPost()?.let { verifyAndConsume(it, relay) }
|
||||
consume(event)
|
||||
|
||||
@@ -45,6 +45,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -99,12 +100,12 @@ val HTTPRegex =
|
||||
class RichTextParser() {
|
||||
fun parseMediaUrl(
|
||||
fullUrl: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
eventTags: ImmutableListOfLists<String>,
|
||||
): ZoomableUrlContent? {
|
||||
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
|
||||
return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
val frags = Nip54().parse(fullUrl)
|
||||
val tags = Nip92().parse(fullUrl, tags.lists)
|
||||
val tags = Nip92().parse(fullUrl, eventTags.lists)
|
||||
|
||||
ZoomableUrlImage(
|
||||
url = fullUrl,
|
||||
@@ -116,7 +117,7 @@ class RichTextParser() {
|
||||
)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
val frags = Nip54().parse(fullUrl)
|
||||
val tags = Nip92().parse(fullUrl, tags.lists)
|
||||
val tags = Nip92().parse(fullUrl, eventTags.lists)
|
||||
ZoomableUrlVideo(
|
||||
url = fullUrl,
|
||||
description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
|
||||
@@ -179,34 +180,34 @@ class RichTextParser() {
|
||||
emojis: Map<String, String>,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
): ImmutableList<ParagraphState> {
|
||||
var paragraphSegments = persistentListOf<ParagraphState>()
|
||||
val lines = content.split('\n')
|
||||
val paragraphSegments = ArrayList<ParagraphState>(lines.size)
|
||||
|
||||
content.split('\n').forEach { paragraph ->
|
||||
var segments = persistentListOf<Segment>()
|
||||
lines.forEach { paragraph ->
|
||||
var isDirty = false
|
||||
|
||||
val isRTL = isArabic(paragraph)
|
||||
|
||||
val wordList = paragraph.trimEnd().split(' ')
|
||||
val segments = ArrayList<Segment>(wordList.size)
|
||||
wordList.forEach { word ->
|
||||
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
|
||||
if (wordSegment !is RegularTextSegment) {
|
||||
isDirty = true
|
||||
}
|
||||
segments = segments.add(wordSegment)
|
||||
segments.add(wordSegment)
|
||||
}
|
||||
|
||||
val newSegments =
|
||||
if (isDirty) {
|
||||
ParagraphState(segments, isRTL)
|
||||
ParagraphState(segments.toPersistentList(), isRTL)
|
||||
} else {
|
||||
ParagraphState(persistentListOf<Segment>(RegularTextSegment(paragraph)), isRTL)
|
||||
}
|
||||
|
||||
paragraphSegments = paragraphSegments.add(newSegments)
|
||||
paragraphSegments.add(newSegments)
|
||||
}
|
||||
|
||||
return paragraphSegments
|
||||
return paragraphSegments.toImmutableList()
|
||||
}
|
||||
|
||||
fun isNumber(word: String): Boolean {
|
||||
@@ -256,7 +257,7 @@ class RichTextParser() {
|
||||
parseHash(word, tags)
|
||||
} else if (word.contains(".") && schemelessMatcher.find()) {
|
||||
val url = schemelessMatcher.group(1) // url
|
||||
val additionalChars = schemelessMatcher.group(4) // additional chars
|
||||
val additionalChars = schemelessMatcher.group(4).ifEmpty { null } // additional chars
|
||||
val pattern =
|
||||
"""^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?"""
|
||||
.toRegex(RegexOption.IGNORE_CASE)
|
||||
@@ -306,7 +307,7 @@ class RichTextParser() {
|
||||
if (hashtagMatcher.find()) {
|
||||
val hashtag = hashtagMatcher.group(1)
|
||||
if (hashtag != null) {
|
||||
return HashTagSegment(word, hashtag, hashtagMatcher.group(2))
|
||||
return HashTagSegment(word, hashtag, hashtagMatcher.group(2).ifEmpty { null })
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType
|
||||
@@ -91,6 +92,7 @@ suspend fun getDocument(
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder().url(url).get().build()
|
||||
HttpClientManager.getHttpClient().newCall(request).execute().use {
|
||||
checkNotInMainThread()
|
||||
if (it.isSuccessful) {
|
||||
val mimeType =
|
||||
it.headers.get("Content-Type")?.toMediaType()
|
||||
|
||||
@@ -113,7 +113,9 @@ class MainActivity : AppCompatActivity() {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel()
|
||||
accountStateViewModel.serviceManager = serviceManager
|
||||
|
||||
LaunchedEffect(key1 = Unit) { accountStateViewModel.tryLoginExistingAccountAsync() }
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountStateViewModel.tryLoginExistingAccountAsync()
|
||||
}
|
||||
|
||||
AccountScreen(accountStateViewModel, sharedPreferencesViewModel)
|
||||
}
|
||||
@@ -158,7 +160,9 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onPause() {
|
||||
Log.d("Lifetime Event", "MainActivity.onPause")
|
||||
|
||||
LanguageTranslatorService.clear()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
LanguageTranslatorService.clear()
|
||||
}
|
||||
serviceManager.cleanObservers()
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
|
||||
@@ -50,8 +50,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -99,7 +97,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
@@ -158,9 +155,9 @@ 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.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
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
|
||||
@@ -264,7 +261,7 @@ fun NewPostView(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(id = R.string.relay_list_selector),
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
@@ -526,23 +523,11 @@ fun NewPostView(
|
||||
}
|
||||
}
|
||||
|
||||
val userSuggestions = postViewModel.userSuggestions
|
||||
if (userSuggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
top = 10.dp,
|
||||
),
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
) {
|
||||
itemsIndexed(
|
||||
userSuggestions,
|
||||
key = { _, item -> item.pubkeyHex },
|
||||
) { _, item ->
|
||||
UserLine(item, accountViewModel) { postViewModel.autocompleteWithUser(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
ShowUserSuggestionList(
|
||||
postViewModel,
|
||||
accountViewModel,
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
)
|
||||
|
||||
BottomRowActions(postViewModel)
|
||||
}
|
||||
@@ -649,7 +634,6 @@ private fun PollField(postViewModel: NewPostViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MessageField(postViewModel: NewPostViewModel) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -1381,7 +1365,7 @@ private fun AddZapraiserButton(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
contentDescription = stringResource(R.string.add_zapraiser),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(13.dp)
|
||||
@@ -1400,7 +1384,7 @@ private fun AddZapraiserButton(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
contentDescription = stringResource(R.string.cancel_zapraiser),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(13.dp)
|
||||
@@ -1423,14 +1407,14 @@ fun AddGeoHash(
|
||||
if (!postViewModel.wantsToAddGeoHash) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOff,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.add_location),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.remove_location),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
@@ -1449,14 +1433,14 @@ private fun AddLnInvoiceButton(
|
||||
if (!isLnInvoiceActive) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CurrencyBitcoin,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.add_bitcoin_invoice),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CurrencyBitcoin,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.cancel_bitcoin_invoice),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange,
|
||||
)
|
||||
@@ -1480,7 +1464,7 @@ private fun ForwardZapTo(
|
||||
if (!postViewModel.wantsForwardZapTo) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
contentDescription = stringResource(R.string.add_zap_split),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(20.dp)
|
||||
@@ -1489,7 +1473,7 @@ private fun ForwardZapTo(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowForwardIos,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(13.dp)
|
||||
@@ -1499,7 +1483,7 @@ private fun ForwardZapTo(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
contentDescription = stringResource(id = R.string.cancel_zap_split),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(20.dp)
|
||||
@@ -1508,7 +1492,7 @@ private fun ForwardZapTo(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ArrowForwardIos,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(13.dp)
|
||||
@@ -1538,7 +1522,7 @@ private fun AddClassifiedsButton(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Sell,
|
||||
contentDescription = stringResource(id = R.string.classifieds),
|
||||
contentDescription = stringResource(id = R.string.cancel_classifieds),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange,
|
||||
)
|
||||
@@ -1562,7 +1546,7 @@ private fun MarkAsSensitive(
|
||||
if (!postViewModel.wantsToMarkAsSensitive) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Visibility,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
contentDescription = stringResource(R.string.add_content_warning),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
@@ -1571,7 +1555,7 @@ private fun MarkAsSensitive(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(10.dp)
|
||||
@@ -1581,7 +1565,7 @@ private fun MarkAsSensitive(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VisibilityOff,
|
||||
contentDescription = stringResource(id = R.string.content_warning),
|
||||
contentDescription = stringResource(id = R.string.remove_content_warning),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
@@ -1590,7 +1574,7 @@ private fun MarkAsSensitive(
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringResource(id = R.string.content_warning),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(10.dp)
|
||||
|
||||
@@ -123,7 +123,7 @@ fun ChannelFabColumn(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringResource(R.string.messages_create_public_chat),
|
||||
contentDescription = stringResource(R.string.messages_create_public_private_chat_desription),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -72,7 +73,7 @@ fun NewCommunityNoteButton(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.new_community_note),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -135,7 +136,7 @@ fun NewImageButton(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.new_short),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
@@ -153,7 +154,10 @@ private fun ShowProgress(postViewModel: NewMediaModel) {
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
)
|
||||
.value,
|
||||
modifier = Size55Modifier.clip(CircleShape).background(MaterialTheme.colorScheme.background),
|
||||
modifier =
|
||||
Size55Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
strokeWidth = 5.dp,
|
||||
)
|
||||
postViewModel.uploadingDescription.value?.let {
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
@@ -58,7 +59,7 @@ fun NewNoteButton(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
null,
|
||||
contentDescription = stringResource(R.string.new_post),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
@@ -298,27 +297,24 @@ fun CreateClickableText(
|
||||
suffix: String?,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overrideColor: Color? = null,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
fontWeight: FontWeight? = null,
|
||||
route: String,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val currentStyle = LocalTextStyle.current
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||
|
||||
val clickablePartStyle =
|
||||
remember(primaryColor, overrideColor) {
|
||||
currentStyle
|
||||
.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight)
|
||||
.toSpanStyle()
|
||||
}
|
||||
SpanStyle(
|
||||
color = overrideColor ?: primaryColor,
|
||||
fontWeight = fontWeight,
|
||||
)
|
||||
|
||||
val nonClickablePartStyle =
|
||||
remember(onBackgroundColor, overrideColor) {
|
||||
currentStyle
|
||||
.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight)
|
||||
.toSpanStyle()
|
||||
}
|
||||
SpanStyle(
|
||||
color = overrideColor ?: onBackgroundColor,
|
||||
fontWeight = fontWeight,
|
||||
)
|
||||
|
||||
val text =
|
||||
remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) {
|
||||
@@ -333,11 +329,45 @@ fun CreateClickableText(
|
||||
ClickableText(
|
||||
text = text,
|
||||
maxLines = maxLines,
|
||||
style = currentStyle,
|
||||
onClick = { nav(route) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClickableText(
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
softWrap: Boolean = true,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
onClick: (Int) -> Unit,
|
||||
) {
|
||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
val pressIndicator =
|
||||
Modifier.pointerInput(onClick) {
|
||||
detectTapGestures { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
onClick(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier.then(pressIndicator),
|
||||
style = style,
|
||||
softWrap = softWrap,
|
||||
overflow = overflow,
|
||||
maxLines = maxLines,
|
||||
onTextLayout = {
|
||||
layoutResult.value = it
|
||||
onTextLayout(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateTextWithEmoji(
|
||||
text: String,
|
||||
|
||||
+11
-39
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -44,6 +43,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ExpandableTextParser
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
@@ -51,9 +51,6 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
const val SHORT_TEXT_LENGTH = 350
|
||||
const val SHORTEN_AFTER_LINES = 10
|
||||
|
||||
@Composable
|
||||
fun ExpandableRichTextViewer(
|
||||
content: String,
|
||||
@@ -66,30 +63,7 @@ fun ExpandableRichTextViewer(
|
||||
) {
|
||||
var showFullText by remember { mutableStateOf(false) }
|
||||
|
||||
val whereToCut =
|
||||
remember(content) {
|
||||
// Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters
|
||||
val firstSpaceAfterCut =
|
||||
content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
val firstNewLineAfterCut =
|
||||
content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
|
||||
// or after SHORTEN_AFTER_LINES lines
|
||||
val numberOfLines = content.count { it == '\n' }
|
||||
|
||||
var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
||||
|
||||
if (numberOfLines > SHORTEN_AFTER_LINES) {
|
||||
val shortContent = content.lines().take(SHORTEN_AFTER_LINES)
|
||||
charactersInLines = 0
|
||||
for (line in shortContent) {
|
||||
// +1 because new line character is omitted from .lines
|
||||
charactersInLines += (line.length + 1)
|
||||
}
|
||||
}
|
||||
|
||||
minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines)
|
||||
}
|
||||
val whereToCut = remember(content) { ExpandableTextParser.computeWhereToCutIfPostIsTooLong(content) }
|
||||
|
||||
val text by
|
||||
remember(content) {
|
||||
@@ -103,17 +77,15 @@ fun ExpandableRichTextViewer(
|
||||
}
|
||||
|
||||
Box {
|
||||
Crossfade(text, label = "ExpandableRichTextViewer") {
|
||||
RichTextViewer(
|
||||
it,
|
||||
canPreview,
|
||||
modifier.align(Alignment.TopStart),
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
RichTextViewer(
|
||||
text,
|
||||
canPreview,
|
||||
modifier.align(Alignment.TopStart),
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
if (content.length > whereToCut && !showFullText) {
|
||||
Row(
|
||||
|
||||
@@ -28,11 +28,10 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
@@ -49,7 +48,6 @@ 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.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
@@ -62,8 +60,10 @@ import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
@@ -174,7 +174,73 @@ fun RichTextViewer(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Preview()
|
||||
@Composable
|
||||
fun RenderRegularPreview() {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColorState =
|
||||
remember {
|
||||
mutableStateOf(backgroundColor)
|
||||
}
|
||||
|
||||
val nav: (String) -> Unit = {}
|
||||
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
RenderRegular(
|
||||
"nostr:npub1e0z776cpe0gllgktjk54fuzv8pdfxmq6smsmh8xd7t8s7n474n9smk0txy but i'm Monthly funding" +
|
||||
" 7 other humans vitor@vitorpamplona.com at the moment so spread #test a bit thin, but won't always be the case.",
|
||||
EmptyTagList,
|
||||
) { word, state ->
|
||||
when (word) {
|
||||
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> {
|
||||
CreateClickableText(
|
||||
word.segmentText.substring(0, 10),
|
||||
"",
|
||||
1,
|
||||
route = "",
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
RenderRegular(
|
||||
"#Amethyst v0.84.1: ncryptsec support (NIP-49)",
|
||||
EmptyTagList,
|
||||
) { word, state ->
|
||||
when (word) {
|
||||
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRegular(
|
||||
content: String,
|
||||
@@ -183,76 +249,66 @@ private fun RenderRegular(
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
RenderRegular(content, tags) { word, state ->
|
||||
if (canPreview) {
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RenderRegular(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
wordRenderer: @Composable (Segment, RichTextViewerState) -> Unit,
|
||||
) {
|
||||
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) }
|
||||
|
||||
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
|
||||
|
||||
val currentTextStyle = LocalTextStyle.current
|
||||
val currentTextColor = LocalContentColor.current
|
||||
|
||||
val textStyle =
|
||||
remember(currentTextStyle, currentTextColor) {
|
||||
remember(currentTextStyle) {
|
||||
currentTextStyle.copy(
|
||||
lineHeight = 1.4.em,
|
||||
color = currentTextStyle.color.takeOrElse { currentTextColor },
|
||||
)
|
||||
}
|
||||
|
||||
val spaceWidth = measureSpaceWidth(textStyle)
|
||||
|
||||
Column {
|
||||
if (canPreview) {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
val direction =
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides
|
||||
if (paragraph.isRTL) {
|
||||
LayoutDirection.Rtl
|
||||
} else {
|
||||
LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides direction) {
|
||||
FlowRow(
|
||||
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
val direction =
|
||||
if (paragraph.isRTL) {
|
||||
LayoutDirection.Rtl
|
||||
} else {
|
||||
LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides direction) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
|
||||
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
},
|
||||
LocalTextStyle provides textStyle,
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
wordRenderer(word, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +337,6 @@ private fun RenderWordWithoutPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
style: TextStyle,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -291,10 +346,10 @@ private fun RenderWordWithoutPreview(
|
||||
is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
// Don't offer to pay invoices
|
||||
is InvoiceSegment -> NormalWord(word.segmentText, style)
|
||||
is InvoiceSegment -> Text(word.segmentText)
|
||||
// Don't offer to withdraw
|
||||
is WithdrawSegment -> NormalWord(word.segmentText, style)
|
||||
is CashuSegment -> NormalWord(word.segmentText, style)
|
||||
is WithdrawSegment -> Text(word.segmentText)
|
||||
is CashuSegment -> Text(word.segmentText)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
|
||||
@@ -302,7 +357,7 @@ private fun RenderWordWithoutPreview(
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +366,6 @@ private fun RenderWordWithPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
style: TextStyle,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -329,7 +383,7 @@ private fun RenderWordWithPreview(
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,23 +401,7 @@ private fun ZoomableContentView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NormalWord(
|
||||
word: String,
|
||||
style: TextStyle,
|
||||
) {
|
||||
BasicText(
|
||||
text = word,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoProtocolUrlRenderer(word: SchemelessUrlSegment) {
|
||||
RenderUrl(word)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderUrl(segment: SchemelessUrlSegment) {
|
||||
private fun NoProtocolUrlRenderer(segment: SchemelessUrlSegment) {
|
||||
ClickableUrl(segment.url, "https://${segment.url}")
|
||||
segment.extras?.let { it1 -> Text(it1) }
|
||||
}
|
||||
@@ -623,14 +661,6 @@ private fun DisplayFullNote(
|
||||
|
||||
@Composable
|
||||
fun HashTag(
|
||||
word: HashTagSegment,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
RenderHashtag(word, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderHashtag(
|
||||
segment: HashTagSegment,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -657,7 +687,7 @@ private fun RenderHashtag(
|
||||
}
|
||||
}
|
||||
|
||||
segment.extras?.ifBlank { "" }?.let { withStyle(regularText) { append(it) } }
|
||||
segment.extras?.let { withStyle(regularText) { append(it) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ fun RobohashFallbackAsyncImage(
|
||||
|
||||
Image(
|
||||
painter = base64Painter,
|
||||
contentDescription = null,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
@@ -34,7 +36,15 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return account.flowHiddenUsers.value.hiddenUsers.map { LocalCache.getOrCreateUser(it) }
|
||||
return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
|
||||
try {
|
||||
LocalCache.getOrCreateUser(it)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("HiddenAccountsFeedFilter", "Failed to parse key $it")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
@@ -180,7 +181,7 @@ private fun NotifiableIcon(
|
||||
Box(route.notifSize) {
|
||||
Icon(
|
||||
painter = painterResource(id = route.icon),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(route.contentDescriptor),
|
||||
modifier = route.iconSize,
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
||||
)
|
||||
@@ -216,7 +217,12 @@ private fun NotificationDotIcon(modifier: Modifier) {
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = Font12SP,
|
||||
modifier = remember { Modifier.wrapContentHeight().align(Alignment.TopEnd) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.core.util.Consumer
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||
@@ -359,31 +360,60 @@ fun AppNavigation(
|
||||
}
|
||||
|
||||
val activity = LocalContext.current.getActivity()
|
||||
var actionableNextPage by remember {
|
||||
mutableStateOf(uriToRoute(activity.intent?.data?.toString()?.ifBlank { null }))
|
||||
|
||||
var currentIntentNextPage by remember {
|
||||
mutableStateOf(activity.intent?.data?.toString()?.ifBlank { null })
|
||||
}
|
||||
actionableNextPage?.let {
|
||||
LaunchedEffect(it) {
|
||||
navController.navigate(it) {
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
}
|
||||
|
||||
currentIntentNextPage?.let { intentNextPage ->
|
||||
var actionableNextPage by remember {
|
||||
mutableStateOf(uriToRoute(intentNextPage))
|
||||
}
|
||||
|
||||
LaunchedEffect(intentNextPage) {
|
||||
if (actionableNextPage != null) {
|
||||
actionableNextPage?.let {
|
||||
navController.navigate(it) {
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
}
|
||||
actionableNextPage = null
|
||||
}
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.invalid_nip19_uri,
|
||||
R.string.invalid_nip19_uri_description,
|
||||
intentNextPage,
|
||||
)
|
||||
}
|
||||
|
||||
currentIntentNextPage = null
|
||||
}
|
||||
actionableNextPage = null
|
||||
}
|
||||
|
||||
DisposableEffect(activity) {
|
||||
val consumer =
|
||||
Consumer<Intent> { intent ->
|
||||
val uri = intent?.data?.toString()
|
||||
val newPage = uriToRoute(uri)
|
||||
if (!uri.isNullOrBlank()) {
|
||||
val newPage = uriToRoute(uri)
|
||||
|
||||
newPage?.let { route ->
|
||||
val currentRoute = getRouteWithArguments(navController)
|
||||
if (!isSameRoute(currentRoute, route)) {
|
||||
navController.navigate(route) {
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
if (newPage != null) {
|
||||
val currentRoute = getRouteWithArguments(navController)
|
||||
if (!isSameRoute(currentRoute, newPage)) {
|
||||
navController.navigate(newPage) {
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
accountViewModel.toast(
|
||||
R.string.invalid_nip19_uri,
|
||||
R.string.invalid_nip19_uri_description,
|
||||
uri,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,23 +542,17 @@ private fun LoggedInUserPictureDrawer(
|
||||
) {
|
||||
val profilePicture by
|
||||
accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
|
||||
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
|
||||
|
||||
val automaticallyShowProfilePicture =
|
||||
remember {
|
||||
accountViewModel.settings.showProfilePictures.value
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = pubkeyHex,
|
||||
robot = accountViewModel.userProfile().pubkeyHex,
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentDescription = stringResource(id = R.string.your_profile_image),
|
||||
modifier = HeaderPictureModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ sealed class Route(
|
||||
val icon: Int,
|
||||
val notifSize: Modifier = Modifier.size(Size23dp),
|
||||
val iconSize: Modifier = Modifier.size(Size20dp),
|
||||
val contentDescriptor: Int = R.string.route,
|
||||
val hasNewItems: (Account, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _ ->
|
||||
false
|
||||
},
|
||||
@@ -80,6 +81,7 @@ sealed class Route(
|
||||
},
|
||||
)
|
||||
.toImmutableList(),
|
||||
contentDescriptor = R.string.route_home,
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
HomeLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
@@ -89,27 +91,31 @@ sealed class Route(
|
||||
Route(
|
||||
route = "Global",
|
||||
icon = R.drawable.ic_globe,
|
||||
contentDescriptor = R.string.route_global,
|
||||
)
|
||||
|
||||
object Search :
|
||||
Route(
|
||||
route = "Search",
|
||||
icon = R.drawable.ic_search,
|
||||
contentDescriptor = R.string.route_search,
|
||||
)
|
||||
|
||||
object Video :
|
||||
Route(
|
||||
route = "Video",
|
||||
icon = R.drawable.ic_video,
|
||||
contentDescriptor = R.string.route_video,
|
||||
)
|
||||
|
||||
object Discover :
|
||||
Route(
|
||||
route = "Discover",
|
||||
icon = R.drawable.ic_sensors,
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
DiscoverLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
// hasNewItems = { accountViewModel, newNotes ->
|
||||
// DiscoverLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
// },
|
||||
contentDescriptor = R.string.route_discover,
|
||||
)
|
||||
|
||||
object Notification :
|
||||
@@ -119,6 +125,7 @@ sealed class Route(
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
NotificationLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
contentDescriptor = R.string.route_notifications,
|
||||
)
|
||||
|
||||
object Message :
|
||||
@@ -128,18 +135,21 @@ sealed class Route(
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
MessagesLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
contentDescriptor = R.string.route_messages,
|
||||
)
|
||||
|
||||
object BlockedUsers :
|
||||
Route(
|
||||
route = "BlockedUsers",
|
||||
icon = R.drawable.ic_security,
|
||||
contentDescriptor = R.string.route_security_filters,
|
||||
)
|
||||
|
||||
object Bookmarks :
|
||||
Route(
|
||||
route = "Bookmarks",
|
||||
icon = R.drawable.ic_bookmarks,
|
||||
contentDescriptor = R.string.route_home,
|
||||
)
|
||||
|
||||
object Profile :
|
||||
|
||||
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -162,7 +163,7 @@ fun BadgeCompose(
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -492,8 +493,14 @@ private fun RenderReply(
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val replyTo by remember { derivedStateOf { note.replyTo?.lastOrNull() } }
|
||||
replyTo?.let { note ->
|
||||
val replyTo =
|
||||
produceState(initialValue = note.replyTo?.lastOrNull()) {
|
||||
accountViewModel.unwrapIfNeeded(value) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
|
||||
replyTo.value?.let { note ->
|
||||
ChatroomMessageCompose(
|
||||
note,
|
||||
null,
|
||||
|
||||
@@ -152,7 +152,7 @@ fun LikeIcon(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.like_description),
|
||||
modifier = iconSizeModifier,
|
||||
tint = grayTint,
|
||||
)
|
||||
@@ -165,7 +165,7 @@ fun RepostedIcon(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.boost_or_quote_description),
|
||||
modifier = modifier,
|
||||
tint = tint,
|
||||
)
|
||||
@@ -198,10 +198,11 @@ fun ZappedIcon(modifier: Modifier) {
|
||||
fun ZapIcon(
|
||||
modifier: Modifier,
|
||||
tint: Color = Color.Unspecified,
|
||||
contentDescriptor: Int = R.string.zap_description,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
contentDescription = stringResource(contentDescriptor),
|
||||
tint = tint,
|
||||
modifier = modifier,
|
||||
)
|
||||
@@ -244,20 +245,26 @@ fun OpenInNewIcon(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandLessIcon(modifier: Modifier) {
|
||||
fun ExpandLessIcon(
|
||||
modifier: Modifier,
|
||||
contentDescriptor: Int,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandLess,
|
||||
null,
|
||||
contentDescription = stringResource(id = contentDescriptor),
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colorScheme.subtleButton,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandMoreIcon(modifier: Modifier) {
|
||||
fun ExpandMoreIcon(
|
||||
modifier: Modifier,
|
||||
contentDescriptor: Int,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
contentDescription = stringResource(id = contentDescriptor),
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colorScheme.subtleButton,
|
||||
)
|
||||
@@ -270,7 +277,7 @@ fun CommentIcon(
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(id = R.string.reply_description),
|
||||
modifier = iconSizeModifier,
|
||||
tint = tint,
|
||||
)
|
||||
@@ -293,7 +300,7 @@ fun ViewCountIcon(
|
||||
fun PollIcon() {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_poll),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.poll),
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
@@ -303,7 +310,7 @@ fun PollIcon() {
|
||||
fun RegularPostIcon() {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_lists),
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.disable_poll),
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
@@ -435,10 +442,10 @@ fun LinkIcon(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalDotsIcon() {
|
||||
fun VerticalDotsIcon(contentDescriptor: Int? = null) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
contentDescription = contentDescriptor?.let { stringResource(id = it) },
|
||||
modifier = Size18Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
@@ -542,11 +542,6 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
author: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val automaticallyShowProfilePicture =
|
||||
remember {
|
||||
accountViewModel.settings.showProfilePictures.value
|
||||
}
|
||||
|
||||
WatchUserMetadata(author) { baseUserPicture ->
|
||||
// Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashFallbackAsyncImage(
|
||||
@@ -555,7 +550,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -2658,7 +2658,7 @@ fun MoreOptionsButton(
|
||||
modifier = Size24Modifier,
|
||||
onClick = enablePopup,
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
VerticalDotsIcon(R.string.note_options)
|
||||
|
||||
NoteDropDownMenu(
|
||||
baseNote,
|
||||
|
||||
@@ -379,9 +379,9 @@ private fun RenderShowIndividualReactionsButton(wantsToSeeReactions: MutableStat
|
||||
label = "RenderShowIndividualReactionsButton",
|
||||
) {
|
||||
if (it) {
|
||||
ExpandLessIcon(modifier = Size22Modifier)
|
||||
ExpandLessIcon(modifier = Size22Modifier, R.string.close_all_reactions_to_this_post)
|
||||
} else {
|
||||
ExpandMoreIcon(modifier = Size22Modifier)
|
||||
ExpandMoreIcon(modifier = Size22Modifier, R.string.open_all_reactions_to_this_post)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -87,7 +89,7 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.expand_relay_list),
|
||||
modifier = ShowMoreRelaysButtonIconModifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
null,
|
||||
contentDescription = stringResource(id = R.string.expand_relay_list),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
@@ -156,7 +156,8 @@ fun RenderRelay(
|
||||
|
||||
val clickableModifier =
|
||||
remember(relay) {
|
||||
Modifier.padding(1.dp)
|
||||
Modifier
|
||||
.padding(1.dp)
|
||||
.size(Size15dp)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
@@ -175,18 +176,21 @@ fun RenderRelay(
|
||||
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,
|
||||
@@ -223,7 +227,7 @@ fun RenderRelayIcon(
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = displayUrl,
|
||||
model = iconUrl,
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
contentDescription = stringResource(id = R.string.relay_info, displayUrl),
|
||||
colorFilter = RelayIconFilter,
|
||||
modifier = iconModifier,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
|
||||
@@ -340,10 +340,11 @@ fun BaseUserPicture(
|
||||
val myIconSize by remember(size) { derivedStateOf { size.div(3.5f) } }
|
||||
|
||||
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
|
||||
LoadUserProfilePicture(baseUser) { userProfilePicture ->
|
||||
LoadUserProfilePicture(baseUser) { userProfilePicture, userName ->
|
||||
InnerUserPicture(
|
||||
userHex = baseUser.pubkeyHex,
|
||||
userPicture = userProfilePicture,
|
||||
userName = userName,
|
||||
size = size,
|
||||
modifier = innerModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -357,17 +358,18 @@ fun BaseUserPicture(
|
||||
@Composable
|
||||
fun LoadUserProfilePicture(
|
||||
baseUser: User,
|
||||
innerContent: @Composable (String?) -> Unit,
|
||||
innerContent: @Composable (String?, String?) -> Unit,
|
||||
) {
|
||||
val userProfile by baseUser.live().profilePictureChanges.observeAsState(baseUser.profilePicture())
|
||||
val userProfile by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
|
||||
|
||||
innerContent(userProfile)
|
||||
innerContent(userProfile?.profilePicture(), userProfile?.bestDisplayName() ?: userProfile?.bestDisplayName())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerUserPicture(
|
||||
userHex: String,
|
||||
userPicture: String?,
|
||||
userName: String?,
|
||||
size: Dp,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -386,7 +388,12 @@ fun InnerUserPicture(
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = userHex,
|
||||
model = userPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentDescription =
|
||||
if (userName != null) {
|
||||
stringResource(id = R.string.profile_image_of_user, userName)
|
||||
} else {
|
||||
stringResource(id = R.string.profile_image)
|
||||
},
|
||||
modifier = myImageModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
|
||||
@@ -275,7 +275,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
refreshChartModel()
|
||||
}
|
||||
|
||||
suspend fun addToStatsSuspend(newNotes: Set<Note>) {
|
||||
suspend fun addToStatsSuspend(newBlockNotes: Set<Set<Note>>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val currentUser = user.pubkeyHex
|
||||
@@ -287,39 +287,41 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
val takenIntoAccount = this.takenIntoAccount.toMutableSet()
|
||||
var hasNewElements = false
|
||||
|
||||
newNotes.forEach {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) {
|
||||
if (noteEvent is ReactionEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
reactions[netDate] = (reactions[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt())
|
||||
boosts[netDate] = (boosts[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is LnZapEvent) {
|
||||
if (
|
||||
noteEvent.isTaggedUser(currentUser)
|
||||
) { // && noteEvent.pubKey != currentUser User might be sending his own receipts
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] =
|
||||
(zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is TextNoteEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
replies[netDate] = (replies[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
newBlockNotes.forEach { newNotes ->
|
||||
newNotes.forEach {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) {
|
||||
if (noteEvent is ReactionEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
reactions[netDate] = (reactions[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt())
|
||||
boosts[netDate] = (boosts[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is LnZapEvent) {
|
||||
if (
|
||||
noteEvent.isTaggedUser(currentUser)
|
||||
) { // && noteEvent.pubKey != currentUser User might be sending his own receipts
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
zaps[netDate] =
|
||||
(zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is TextNoteEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
replies[netDate] = (replies[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,7 +424,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) { addToStatsSuspend(it.flatten().toSet()) }
|
||||
bundlerInsert.invalidateList(newItems) { addToStatsSuspend(it) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
@@ -213,7 +213,10 @@ fun DrawPlayName(name: String) {
|
||||
@Composable
|
||||
fun DrawPlayNameIcon(onClick: () -> Unit) {
|
||||
IconButton(onClick = onClick, modifier = StdButtonSizeModifier) {
|
||||
PlayIcon(modifier = StdButtonSizeModifier, tint = MaterialTheme.colorScheme.placeholderText)
|
||||
PlayIcon(
|
||||
modifier = StdButtonSizeModifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
@@ -193,6 +194,42 @@ class AccountStateViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun login(
|
||||
key: String,
|
||||
password: String,
|
||||
useProxy: Boolean,
|
||||
proxyPort: Int,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (key.startsWith("ncryptsec")) {
|
||||
val newKey =
|
||||
try {
|
||||
CryptoUtils.decryptNIP49(key, password)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(e.message)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (newKey == null) {
|
||||
onError("Could not decrypt key with provided password")
|
||||
Log.e("Login", "Could not decrypt ncryptsec")
|
||||
} else {
|
||||
loginSync(newKey, useProxy, proxyPort, loginWithExternalSigner, packageName) {
|
||||
onError(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName) {
|
||||
onError(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(
|
||||
key: String,
|
||||
useProxy: Boolean,
|
||||
@@ -202,13 +239,24 @@ class AccountStateViewModel() : ViewModel() {
|
||||
onError: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
onError()
|
||||
}
|
||||
loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loginSync(
|
||||
key: String,
|
||||
useProxy: Boolean,
|
||||
proxyPort: Int,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
onError: () -> Unit,
|
||||
) {
|
||||
try {
|
||||
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
onError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,8 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -396,13 +398,13 @@ fun NoteMaster(
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||
modifier = Size24Modifier,
|
||||
onClick = enablePopup,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
|
||||
+234
-6
@@ -36,23 +36,48 @@ 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.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
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 androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
@@ -67,10 +92,14 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.note.authenticate
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNsec
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun AccountBackupDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -80,12 +109,23 @@ fun AccountBackupDialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxSize(),
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -93,7 +133,10 @@ fun AccountBackupDialog(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 30.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 30.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
@@ -101,13 +144,105 @@ fun AccountBackupDialog(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips_md),
|
||||
content = stringResource(R.string.account_backup_tips2_md),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
NSecCopyButton(accountViewModel)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
Material3RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips3_md),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { password.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.ncryptsec_password),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
)
|
||||
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EncryptNSecCopyButton(accountViewModel, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +295,56 @@ private fun NSecCopyButton(accountViewModel: AccountViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EncryptNSecCopyButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
password: MutableState<TextFieldValue>,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.copy_my_secret_key),
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
contentPadding = ButtonPadding,
|
||||
enabled = password.value.text.isNotBlank(),
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription =
|
||||
stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
||||
modifier = Modifier.padding(end = 5.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.encrypt_and_copy_my_secret_key),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getFragmentActivity(): FragmentActivity? {
|
||||
var currentContext = this
|
||||
while (currentContext is ContextWrapper) {
|
||||
@@ -189,3 +374,46 @@ private fun copyNSec(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun encryptCopyNSec(
|
||||
password: MutableState<TextFieldValue>,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
accountViewModel: AccountViewModel,
|
||||
clipboardManager: ClipboardManager,
|
||||
) {
|
||||
if (password.value.text.isBlank()) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.password_is_required),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
accountViewModel.account.keyPair.privKey?.let {
|
||||
val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text)
|
||||
if (key != null) {
|
||||
clipboardManager.setText(AnnotatedString(key))
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.secret_key_copied_to_clipboard),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.failed_to_encrypt_key),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
-1
@@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -104,7 +105,11 @@ import kotlin.time.measureTimedValue
|
||||
|
||||
@Immutable class StringToastMsg(val title: String, val msg: String) : ToastMsg()
|
||||
|
||||
@Immutable class ResourceToastMsg(val titleResId: Int, val resourceId: Int) : ToastMsg()
|
||||
@Immutable class ResourceToastMsg(
|
||||
val titleResId: Int,
|
||||
val resourceId: Int,
|
||||
val params: Array<out String>? = null,
|
||||
) : ToastMsg()
|
||||
|
||||
@Stable
|
||||
class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao {
|
||||
@@ -140,6 +145,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId)) }
|
||||
}
|
||||
|
||||
fun toast(
|
||||
titleResId: Int,
|
||||
resourceId: Int,
|
||||
vararg params: String,
|
||||
) {
|
||||
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) }
|
||||
}
|
||||
|
||||
fun isWriteable(): Boolean {
|
||||
return account.isWriteable()
|
||||
}
|
||||
@@ -1175,6 +1188,55 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrapIfNeeded(
|
||||
event: EventInterface?,
|
||||
onReady: (Note) -> Unit,
|
||||
) {
|
||||
when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
event.cachedGift(account.signer) {
|
||||
val existingNote = LocalCache.getNoteIfExists(it.id)
|
||||
if (existingNote != null) {
|
||||
unwrapIfNeeded(existingNote.event, onReady)
|
||||
} else {
|
||||
LocalCache.verifyAndConsume(it, null)
|
||||
unwrapIfNeeded(it, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
event.cachedGossip(account.signer) {
|
||||
val existingNote = LocalCache.getNoteIfExists(it.id)
|
||||
if (existingNote != null) {
|
||||
unwrapIfNeeded(existingNote.event, onReady)
|
||||
} else {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
unwrapIfNeeded(it, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
event?.id()?.let {
|
||||
LocalCache.getNoteIfExists(it)?.let {
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrapIfNeeded(
|
||||
note: Note?,
|
||||
onReady: (Note) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
unwrapIfNeeded(note?.event) {
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
|
||||
|
||||
@@ -38,10 +38,12 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
@@ -112,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
@@ -269,7 +272,13 @@ fun ChannelScreen(
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
Column(
|
||||
modifier = remember { Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
},
|
||||
) {
|
||||
if (channel is LiveActivitiesChannel) {
|
||||
ShowVideoStreaming(channel, accountViewModel)
|
||||
@@ -340,7 +349,11 @@ fun DisplayReplyingToNote(
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 10.dp).animateContentSize(),
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.heightIn(max = 100.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (replyingNote != null) {
|
||||
@@ -363,7 +376,10 @@ fun DisplayReplyingToNote(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier.padding(end = 5.dp).size(30.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(30.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
@@ -379,13 +395,13 @@ fun EditFieldRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = EditFieldModifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ShowUserSuggestionList(channelScreenModel, accountViewModel)
|
||||
|
||||
MyTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
@@ -394,7 +410,7 @@ fun EditFieldRow(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.reply_here),
|
||||
@@ -431,6 +447,7 @@ fun EditFieldRow(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -725,7 +742,11 @@ fun ShortChannelHeader(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 10.dp).height(35.dp).weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.height(35.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -738,7 +759,10 @@ fun ShortChannelHeader(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(Size35dp).padding(start = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(Size35dp)
|
||||
.padding(start = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (channel is PublicChatChannel) {
|
||||
@@ -1028,7 +1052,12 @@ fun LiveFlag() {
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
remember { Modifier.clip(SmallBorder).background(Color.Red).padding(horizontal = 5.dp) },
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Red)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1039,7 +1068,12 @@ fun EndedFlag() {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier =
|
||||
remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) },
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1050,7 +1084,12 @@ fun OfflineFlag() {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier =
|
||||
remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) },
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1064,7 +1103,12 @@ fun ScheduledFlag(starts: Long?) {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier =
|
||||
remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) },
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1073,7 +1117,10 @@ private fun NoteCopyButton(note: Channel) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { popupExpanded = true },
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
@@ -1116,7 +1163,10 @@ private fun EditButton(
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { wantsToPost = true },
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
|
||||
@@ -25,11 +25,13 @@ import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -92,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayRoomSubject
|
||||
@@ -363,13 +366,13 @@ fun PrivateMessageEditFieldRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = EditFieldModifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ShowUserSuggestionList(channelScreenModel, accountViewModel)
|
||||
|
||||
MyTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
@@ -378,7 +381,7 @@ fun PrivateMessageEditFieldRow(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.reply_here),
|
||||
@@ -461,10 +464,36 @@ fun PrivateMessageEditFieldRow(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowUserSuggestionList(
|
||||
channelScreenModel: NewPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||
) {
|
||||
val userSuggestions = channelScreenModel.userSuggestions
|
||||
if (userSuggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
top = 10.dp,
|
||||
),
|
||||
modifier = modifier,
|
||||
) {
|
||||
itemsIndexed(
|
||||
userSuggestions,
|
||||
key = { _, item -> item.pubkeyHex },
|
||||
) { _, item ->
|
||||
UserLine(item, accountViewModel) { channelScreenModel.autocompleteWithUser(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewFeatureNIP24AlertDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
|
||||
@@ -270,6 +270,7 @@ fun MainScreen(
|
||||
videoFeedViewModel.sendToTop()
|
||||
}
|
||||
Route.Discover.base -> {
|
||||
discoverMarketplaceFeedViewModel.sendToTop()
|
||||
discoveryLiveFeedViewModel.sendToTop()
|
||||
discoveryCommunityFeedViewModel.sendToTop()
|
||||
discoveryChatFeedViewModel.sendToTop()
|
||||
@@ -472,12 +473,22 @@ private fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
|
||||
openDialogMsg.value?.let { obj ->
|
||||
when (obj) {
|
||||
is ResourceToastMsg ->
|
||||
InformationDialog(
|
||||
context.getString(obj.titleResId),
|
||||
context.getString(obj.resourceId),
|
||||
) {
|
||||
accountViewModel.clearToasts()
|
||||
if (obj.params != null) {
|
||||
InformationDialog(
|
||||
context.getString(obj.titleResId),
|
||||
context.getString(obj.resourceId, *obj.params),
|
||||
) {
|
||||
accountViewModel.clearToasts()
|
||||
}
|
||||
} else {
|
||||
InformationDialog(
|
||||
context.getString(obj.titleResId),
|
||||
context.getString(obj.resourceId),
|
||||
) {
|
||||
accountViewModel.clearToasts()
|
||||
}
|
||||
}
|
||||
|
||||
is StringToastMsg ->
|
||||
InformationDialog(
|
||||
obj.title,
|
||||
|
||||
@@ -155,7 +155,9 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -390,7 +392,10 @@ private fun RenderSurface(
|
||||
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().onSizeChanged { columnSize = it },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { columnSize = it },
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -403,7 +408,8 @@ private fun RenderSurface(
|
||||
Box(
|
||||
modifier =
|
||||
remember {
|
||||
Modifier.verticalScroll(scrollState)
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.nestedScroll(
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
@@ -726,10 +732,17 @@ private fun ProfileHeader(
|
||||
DrawBanner(baseUser, accountViewModel)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 10.dp).size(40.dp).align(Alignment.TopEnd),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.size(40.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.size(30.dp).align(Alignment.Center),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.Center),
|
||||
onClick = { popupExpanded = true },
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
@@ -754,7 +767,11 @@ private fun ProfileHeader(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp).padding(top = 75.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = 75.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -789,7 +806,10 @@ private fun ProfileHeader(
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(Size35dp).padding(bottom = 3.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp),
|
||||
) {
|
||||
MessageButton(baseUser, accountViewModel, nav)
|
||||
|
||||
@@ -969,13 +989,16 @@ private fun DrawAdditionalInfo(
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(25.dp).padding(start = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(25.dp)
|
||||
.padding(start = 5.dp),
|
||||
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
contentDescription = stringResource(id = R.string.copy_npub_to_clipboard),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
@@ -995,13 +1018,13 @@ private fun DrawAdditionalInfo(
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(25.dp),
|
||||
modifier = Size25Modifier,
|
||||
onClick = { dialogOpen = true },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
contentDescription = stringResource(id = R.string.show_npub_as_a_qr_code),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
@@ -1059,7 +1082,10 @@ private fun DrawAdditionalInfo(
|
||||
text = AnnotatedString(identity.identity),
|
||||
onClick = { runCatching { uri.openUri(identity.toProofUrl()) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp).weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1131,7 +1157,10 @@ fun DisplayLNAddress(
|
||||
text = AnnotatedString(lud16),
|
||||
onClick = { zapExpanded = !zapExpanded },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp).weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1223,12 +1252,21 @@ private fun WatchApp(
|
||||
|
||||
appLogo?.let {
|
||||
Box(
|
||||
remember { Modifier.size(Size35dp).clickable { nav("Note/${baseApp.idHex}") } },
|
||||
remember {
|
||||
Modifier
|
||||
.size(Size35dp)
|
||||
.clickable { nav("Note/${baseApp.idHex}") }
|
||||
},
|
||||
) {
|
||||
AsyncImage(
|
||||
model = appLogo,
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(Size35dp).clip(shape = CircleShape) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.size(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1343,7 +1381,11 @@ fun BadgeThumb(
|
||||
onClick: ((String) -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
remember { Modifier.width(size).height(size) },
|
||||
remember {
|
||||
Modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
},
|
||||
) {
|
||||
WatchAndRenderBadgeImage(baseNote, loadProfilePicture, size, pictureModifier, onClick)
|
||||
}
|
||||
@@ -1373,7 +1415,13 @@ private fun WatchAndRenderBadgeImage(
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = remember { pictureModifier.width(size).height(size).background(bgColor) },
|
||||
modifier =
|
||||
remember {
|
||||
pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.background(bgColor)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
RobohashFallbackAsyncImage(
|
||||
@@ -1418,7 +1466,8 @@ fun DrawBanner(
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(125.dp)
|
||||
.combinedClickable(
|
||||
onClick = { zoomImageDialogOpen = true },
|
||||
@@ -1438,7 +1487,10 @@ fun DrawBanner(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = stringResource(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth().height(125.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(125.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1692,7 +1744,10 @@ private fun MessageButton(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) { accountViewModel.createChatRoomFor(user) { nav("Room/$it") } }
|
||||
},
|
||||
@@ -1727,7 +1782,10 @@ private fun InnerEditButtonPreview() {
|
||||
@Composable
|
||||
private fun InnerEditButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = onClick,
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -90,10 +91,12 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
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.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
@@ -142,6 +145,16 @@ fun LoginPage(
|
||||
val scope = rememberCoroutineScope()
|
||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||
|
||||
var processingLogin by remember { mutableStateOf(false) }
|
||||
|
||||
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val needsPassword =
|
||||
remember {
|
||||
derivedStateOf {
|
||||
key.value.text.startsWith("ncryptsec1")
|
||||
}
|
||||
}
|
||||
|
||||
if (loginWithExternalSigner) {
|
||||
val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
|
||||
val id = remember { UUID.randomUUID().toString() }
|
||||
@@ -243,33 +256,47 @@ fun LoginPage(
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
var showCharsKey by remember { mutableStateOf(false) }
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
val autofillNodeKey =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
|
||||
val autofillNodePassword =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
LocalAutofillTree.current += autofillNodeKey
|
||||
LocalAutofillTree.current += autofillNodePassword
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
autofillNodeKey.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
requestAutofillForNode(autofillNodeKey)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
cancelAutofillForNode(autofillNodeKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = key.value,
|
||||
onValueChange = { key.value = it },
|
||||
onValueChange = {
|
||||
key.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
@@ -284,12 +311,12 @@ fun LoginPage(
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
IconButton(onClick = { showCharsKey = !showCharsKey }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
if (showCharsKey) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
@@ -312,29 +339,42 @@ fun LoginPage(
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
contentDescription =
|
||||
stringResource(
|
||||
R.string.login_with_qr_code,
|
||||
),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
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()) {
|
||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
if (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
context.getString(R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -350,39 +390,128 @@ fun LoginPage(
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
if (needsPassword.value) {
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNodePassword.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.connect_via_tor))
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNodePassword)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNodePassword)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.ncryptsec_password),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
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 (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
context.getString(R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.connect_via_tor))
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,19 +574,33 @@ fun LoginPage(
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
if (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
context.getString(R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
modifier = Modifier.padding(horizontal = 40.dp),
|
||||
)
|
||||
Row(modifier = Modifier.padding(horizontal = 40.dp)) {
|
||||
if (processingLogin) {
|
||||
LoadingAnimation()
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
Text(stringResource(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ val Size19Modifier = Modifier.size(19.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size25Modifier = Modifier.size(25.dp)
|
||||
val Size26Modifier = Modifier.size(26.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size35Modifier = Modifier.size(35.dp)
|
||||
|
||||
@@ -193,12 +193,6 @@
|
||||
<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>
|
||||
|
||||
@@ -182,12 +182,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Nasměrujte na QR kód</string>
|
||||
<string name="show_qr">Zobrazit QR kód</string>
|
||||
<string name="profile_image">Profilový obrázek</string>
|
||||
<string name="your_profile_image">Profilový obrázek</string>
|
||||
<string name="scan_qr">Skenovat QR kód</string>
|
||||
<string name="show_anyway">Zobrazit přesto</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Příspěvek byl označen jako nevhodný uživatelem</string>
|
||||
@@ -138,12 +139,15 @@
|
||||
<string name="clear">Vymazat</string>
|
||||
<string name="app_logo">Logo aplikace</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex privátní klíč</string>
|
||||
<string name="ncryptsec_password">heslo pro otevření klíče</string>
|
||||
<string name="show_password">Zobrazit heslo</string>
|
||||
<string name="hide_password">Skrýt heslo</string>
|
||||
<string name="invalid_key">Neplatný klíč</string>
|
||||
<string name="invalid_key_with_message">Neplatný klíč: %1$s</string>
|
||||
<string name="i_accept_the">"Souhlasím s "</string>
|
||||
<string name="terms_of_use">podmínkami použití</string>
|
||||
<string name="acceptance_of_terms_is_required">Je vyžadováno přijetí podmínek</string>
|
||||
<string name="password_is_required">Heslo je vyžadováno</string>
|
||||
<string name="key_is_required">Klíč je povinný</string>
|
||||
<string name="name_is_required">Jméno je povinné</string>
|
||||
<string name="login">Přihlásit se</string>
|
||||
@@ -194,14 +198,17 @@
|
||||
<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
|
||||
\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íč.
|
||||
\n- **Uchovávejte** si bezpečnou zálohu svého tajného klíče pro obnovení účtu. Doporučujeme používat správce hesel.
|
||||
</string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos"> ## Tipy pro zálohování klíčů a bezpečnost
|
||||
\n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu.
|
||||
\n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete.
|
||||
\n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč.
|
||||
\n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel.</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla.
|
||||
\n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč.</string>
|
||||
<string name="failed_to_encrypt_key">Nepodařilo se zašifrovat váš privátní klíč</string>
|
||||
<string name="secret_key_copied_to_clipboard">Tajný klíč (nsec) zkopírován do schránky</string>
|
||||
<string name="copy_my_secret_key">Zkopírovat můj tajný klíč</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Zašifrovat a zkopírovat můj tajný klíč</string>
|
||||
<string name="biometric_authentication_failed">Autentizace se nezdařila</string>
|
||||
<string name="biometric_authentication_failed_explainer">Biometrické údaje se nepodařilo ověřit vlastníka tohoto telefonu</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">Biometrické údaje se nepodařilo ověřit vlastníka tohoto telefonu. Chyba: %1$s</string>
|
||||
@@ -453,6 +460,7 @@
|
||||
<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_create_public_private_chat_desription">Nová veřejná nebo soukromá skupina</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>
|
||||
@@ -469,6 +477,7 @@
|
||||
<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_to_clipboard">Kopírovat do schránky</string>
|
||||
<string name="copy_npub_to_clipboard">Kopírovat npub do schránky</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>
|
||||
@@ -597,4 +606,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Server po nahrání neposkytl URL</string>
|
||||
<string name="could_not_download_from_the_server">Nepodařilo se stáhnout nahraná média ze serveru</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Nelze připravit místní soubor k nahrání: %1$s</string>
|
||||
<string name="login_with_qr_code">Přihlášení pomocí QR kódu</string>
|
||||
<string name="route">Trasa</string>
|
||||
<string name="route_home">Domů</string>
|
||||
<string name="route_search">Hledat</string>
|
||||
<string name="route_discover">Objevit</string>
|
||||
<string name="route_messages">Zprávy</string>
|
||||
<string name="route_notifications">Upozornění</string>
|
||||
<string name="route_global">Globální</string>
|
||||
<string name="route_video">Krátké</string>
|
||||
<string name="route_security_filters">Bezpečnostní filtry</string>
|
||||
<string name="new_post">Nový příspěvek</string>
|
||||
<string name="new_short">Nové Shorts: obrázky nebo videa</string>
|
||||
<string name="new_community_note">Nová poznámka komunity</string>
|
||||
<string name="open_all_reactions_to_this_post">Otevřít všechny reakce pro tento příspěvek</string>
|
||||
<string name="close_all_reactions_to_this_post">Zavřít všechny reakce na tento příspěvek</string>
|
||||
<string name="reply_description">Odpověď</string>
|
||||
<string name="boost_or_quote_description">Zvýšit nebo citovat</string>
|
||||
<string name="like_description">Olajkovat</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">Obrázek profilu %1$s</string>
|
||||
<string name="relay_info">Relé %1$s</string>
|
||||
<string name="expand_relay_list">Rozbalit seznam relací</string>
|
||||
<string name="note_options">Volby poznámky</string>
|
||||
<string name="relay_list_selector">Výběr seznamu relé</string>
|
||||
<string name="poll">Anketa</string>
|
||||
<string name="disable_poll">Vypnout anketu</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoinová faktura</string>
|
||||
<string name="cancel_bitcoin_invoice">Zrušit Bitcoinovou fakturu</string>
|
||||
<string name="cancel_classifieds">Zrušit prodej položky</string>
|
||||
<string name="add_zapraiser">Zap-sběr</string>
|
||||
<string name="cancel_zapraiser">Zrušit Zap-zběrku</string>
|
||||
<string name="add_location">Místo</string>
|
||||
<string name="remove_location">Odstranit umístění</string>
|
||||
<string name="add_zap_split">Zap splits</string>
|
||||
<string name="cancel_zap_split">Zrušit rozdělení Zap</string>
|
||||
<string name="add_content_warning">Přidat upozornění na obsah</string>
|
||||
<string name="remove_content_warning">Odstranit upozornění na obsah</string>
|
||||
<string name="show_npub_as_a_qr_code">Zobrazit npub jako QR kód</string>
|
||||
<string name="invalid_nip19_uri">Neplatná adresa</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst obdržel URI k otevření, ale tento URI byl neplatný: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Zeigen Sie auf den QR Code</string>
|
||||
<string name="show_qr">QR Code anzeigen</string>
|
||||
<string name="profile_image">Profilbild</string>
|
||||
<string name="your_profile_image">Dein Profilbild</string>
|
||||
<string name="scan_qr">QR Code scannen</string>
|
||||
<string name="show_anyway">Trotzdem anzeigen</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Der Beitrag wurde als unangemessen gekennzeichnet von</string>
|
||||
@@ -140,14 +141,17 @@ erie gespeichert</string>
|
||||
<string name="clear">Löschen</string>
|
||||
<string name="app_logo">App-Logo</string>
|
||||
<string name="nsec_npub_hex_private_key">Nsec / Npub / Hex-Privatschlüssel</string>
|
||||
<string name="ncryptsec_password">Passwort zum Öffnen des Schlüssels</string>
|
||||
<string name="show_password">Passwort anzeigen</string>
|
||||
<string name="hide_password">Passwort ausblenden</string>
|
||||
<string name="invalid_key">Ungültiger Schlüssel</string>
|
||||
<string name="invalid_key_with_message">Ungültiger Schlüssel: %1$s</string>
|
||||
<string name="i_accept_the">"Ich akzeptiere die "</string>
|
||||
<string name="terms_of_use">Nutzungsbedingungen</string>
|
||||
<string name="acceptance_of_terms_is_required">Die Akzept
|
||||
|
||||
anz der Bedingungen ist erforderlich</string>
|
||||
<string name="password_is_required">Passwort ist erforderlich</string>
|
||||
<string name="key_is_required">Schlüssel ist erforderlich</string>
|
||||
<string name="name_is_required">Ein Name wird benötigt</string>
|
||||
<string name="login">Anmeldung</string>
|
||||
@@ -198,13 +202,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
|
||||
\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.
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos"> ## Schlüsselsicherheits-Tipps
|
||||
\n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern.
|
||||
\n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der 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 Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden.
|
||||
\n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen.</string>
|
||||
<string name="failed_to_encrypt_key">Fehler beim Verschlüsseln Ihres privaten Schlüssels</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="encrypt_and_copy_my_secret_key">Verschlüsseln und kopieren Sie meinen geheimen Schlüssel</string>
|
||||
<string name="biometric_authentication_failed">Authentifizierung fehlgeschlagen</string>
|
||||
<string name="biometric_authentication_failed_explainer">Biometrie konnte den Besitzer dieses Telefons nicht authentifizieren</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">Biometrie konnte den Besitzer dieses Telefons nicht authentifizieren. Fehler: %1$s</string>
|
||||
@@ -457,6 +465,7 @@ anz der Bedingungen ist erforderlich</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_create_public_private_chat_desription">Neue öffentliche oder private Gruppe</string>
|
||||
<string name="messages_new_message">Privat</string>
|
||||
<string name="messages_new_message_to">An</string>
|
||||
<string name="messages_new_message_subject">Betreff</string>
|
||||
@@ -473,6 +482,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="automatically_show_url_preview_description">URL-Vorschauen anzeigen</string>
|
||||
<string name="load_image_description">Wann Bilder geladen werden sollen</string>
|
||||
<string name="copy_to_clipboard">In Zwischenablage kopieren</string>
|
||||
<string name="copy_npub_to_clipboard">Npub in Zwischenablage kopieren</string>
|
||||
<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>
|
||||
@@ -601,4 +611,44 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Der Server hat nach dem Hochladen keine URL angegeben</string>
|
||||
<string name="could_not_download_from_the_server">Hochgeladene Medien konnten nicht vom Server heruntergeladen werden</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s</string>
|
||||
<string name="login_with_qr_code">Einloggen mit QR-Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Startseite</string>
|
||||
<string name="route_search">Suche</string>
|
||||
<string name="route_discover">Entdecken</string>
|
||||
<string name="route_messages">Nachrichten</string>
|
||||
<string name="route_notifications">Benachrichtigungen</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Kurzfilme</string>
|
||||
<string name="route_security_filters">Sicherheitsfilter</string>
|
||||
<string name="new_post">Neuer Beitrag</string>
|
||||
<string name="new_short">Neue Kurzfilme: Bilder oder Videos</string>
|
||||
<string name="new_community_note">Neue Community-Notiz</string>
|
||||
<string name="open_all_reactions_to_this_post">Alle Reaktionen auf diesen Beitrag öffnen</string>
|
||||
<string name="close_all_reactions_to_this_post">Alle Reaktionen auf diesen Beitrag schließen</string>
|
||||
<string name="reply_description">Antworten</string>
|
||||
<string name="boost_or_quote_description">Boosten oder Zitieren</string>
|
||||
<string name="like_description">Gefällt mir</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">Profilbild von %1$s</string>
|
||||
<string name="relay_info">%1$s weiterleiten</string>
|
||||
<string name="expand_relay_list">Rela-Liste erweitern</string>
|
||||
<string name="note_options">Notizoptionen</string>
|
||||
<string name="relay_list_selector">Rela-Listenauswahl</string>
|
||||
<string name="poll">Umfrage</string>
|
||||
<string name="disable_poll">Umfrage deaktivieren</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoin-Rechnung hinzufügen</string>
|
||||
<string name="cancel_bitcoin_invoice">Bitcoin-Rechnung abbrechen</string>
|
||||
<string name="cancel_classifieds">Verkauf eines Artikels abbrechen</string>
|
||||
<string name="add_zapraiser">Zapraiser hinzufügen</string>
|
||||
<string name="cancel_zapraiser">Zapraiser abbrechen</string>
|
||||
<string name="add_location">Ort hinzufügen</string>
|
||||
<string name="remove_location">Ort entfernen</string>
|
||||
<string name="add_zap_split">Zap-Aufteilung hinzufügen</string>
|
||||
<string name="cancel_zap_split">Zap-Aufteilung abbrechen</string>
|
||||
<string name="add_content_warning">Inhaltswarnung hinzufügen</string>
|
||||
<string name="remove_content_warning">Inhaltswarnung entfernen</string>
|
||||
<string name="show_npub_as_a_qr_code">Npub als QR-Code anzeigen</string>
|
||||
<string name="invalid_nip19_uri">Ungültige Adresse</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst hat eine URI zum Öffnen erhalten, aber diese URI war ungültig: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -185,12 +185,6 @@
|
||||
<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>
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">Point to the QR Code</string>
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="profile_image">Profile Image</string>
|
||||
<string name="scan_qr">Scan QR</string>
|
||||
<string name="show_anyway">Show Anyway</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post was muted or reported by</string>
|
||||
<string name="post_not_found">Event is loading or can\'t be found in your relay list</string>
|
||||
<string name="channel_image">Channel Image</string>
|
||||
<string name="referenced_event_not_found">Referenced event not found</string>
|
||||
<string name="could_not_decrypt_the_message">Could not decrypt the message</string>
|
||||
<string name="group_picture">Group Picture</string>
|
||||
<string name="explicit_content">Explicit Content</string>
|
||||
</resources>
|
||||
|
||||
@@ -182,12 +182,6 @@
|
||||
<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
|
||||
\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.
|
||||
\n- **Ja** konservu sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortmanaĝilon.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Sekreta ŝlosilo (nsec) kopiita al tondujo</string>
|
||||
<string name="copy_my_secret_key">Kopii mian sekretan ŝlosilon</string>
|
||||
<string name="biometric_authentication_failed">Aŭtentikigo malsukcesis</string>
|
||||
|
||||
@@ -194,12 +194,6 @@
|
||||
<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="account_backup_tips_md" tools:ignore="Typos"> ## Consejos sobre la protección y la copia de seguridad de las claves
|
||||
\n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad.
|
||||
\n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes.
|
||||
\n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta.
|
||||
\n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.
|
||||
</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>
|
||||
|
||||
@@ -194,12 +194,6 @@
|
||||
<string name="mark_all_new_as_read">Marcar todos los nuevos como leídos</string>
|
||||
<string name="mark_all_as_read">Marcar todos como leídos</string>
|
||||
<string name="backup_keys">Respaldar claves</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Consejos sobre la protección y la copia de seguridad de las claves
|
||||
\n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad.
|
||||
\n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes.
|
||||
\n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta.
|
||||
\n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.
|
||||
</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">Error de autenticación</string>
|
||||
|
||||
@@ -194,12 +194,6 @@
|
||||
<string name="mark_all_new_as_read">Marcar todos los nuevos como leídos</string>
|
||||
<string name="mark_all_as_read">Marcar todos como leídos</string>
|
||||
<string name="backup_keys">Respaldar claves</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Consejos sobre la protección y la copia de seguridad de las claves
|
||||
\n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad.
|
||||
\n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes.
|
||||
\n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta.
|
||||
\n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.
|
||||
</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">Error de autenticación</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">افزودن رله</string>
|
||||
<string name="display_name">نمایش نام</string>
|
||||
<string name="my_display_name">نام من</string>
|
||||
<string name="my_awesome_name">استریچ باحالیان</string>
|
||||
<string name="welcome">خوش آمدی استریچ!</string>
|
||||
<string name="username">نام کاربری</string>
|
||||
<string name="my_username">نام کاربری من</string>
|
||||
<string name="about_me">درباره من</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">شرایط استفاده</string>
|
||||
<string name="acceptance_of_terms_is_required">پذیرش شرایط الازمیست</string>
|
||||
<string name="key_is_required">کلید الزامیست</string>
|
||||
<string name="name_is_required">نام الزامی است</string>
|
||||
<string name="login">ورود</string>
|
||||
<string name="sign_up">ثبت نام</string>
|
||||
<string name="create_account">ایجاد حساب کاربری</string>
|
||||
<string name="how_should_we_call_you">شما را چی صدا بزنیم؟</string>
|
||||
<string name="don_t_have_an_account">حساب کاربری ناستر ندارید؟</string>
|
||||
<string name="already_have_an_account">از قبل حساب کاربری ناستر دارید؟</string>
|
||||
<string name="create_a_new_account">ایجاد حساب کاربری جدید</string>
|
||||
<string name="generate_a_new_key">ساختن جفت کلید جدید</string>
|
||||
<string name="loading_feed">در حال بارگیری خبرنامه</string>
|
||||
<string name="loading_account">بارگذاری حساب کاربری</string>
|
||||
@@ -185,12 +194,6 @@
|
||||
<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- سازندگان آماتیست **هرگز** از شما کلید خصوصی تان را نمی خواهند.
|
||||
\n- حتما یک کپی از کلید خصوصی را برای بازیابی حساب کاربری نزد خود نگه دارد. ما استفاده از یک نرم افزار مدیریت پسورد را توصیه می نماییم.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">کلید خصوصی در کلیپبورد کپی شد</string>
|
||||
<string name="copy_my_secret_key">کلید خصوصیم را کپی کن</string>
|
||||
<string name="biometric_authentication_failed">احراز هویت انجام نشد</string>
|
||||
@@ -318,6 +321,7 @@
|
||||
<string name="follow_list_selection">لیست دنبال ها</string>
|
||||
<string name="follow_list_kind3follows">همه دنبال ها</string>
|
||||
<string name="follow_list_global">همگانی</string>
|
||||
<string name="follow_list_mute_list">لیست خموش</string>
|
||||
<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
|
||||
@@ -385,6 +389,9 @@
|
||||
<string name="payment">پرداخت</string>
|
||||
<string name="cashu">توکن Cashu</string>
|
||||
<string name="cashu_redeem">بازپرداخت</string>
|
||||
<string name="cashu_redeem_to_zap">ارسال به کیف پول زپ</string>
|
||||
<string name="cashu_redeem_to_cashu">باز کردن در کیف پول Cashu</string>
|
||||
<string name="cashu_copy_token">کپی کردن توکن</string>
|
||||
<string name="no_lightning_address_set">آدرس لایتنینگ تنظیم نشده است</string>
|
||||
<string name="copied_token_to_clipboard">توکن به کلیپبورد کپی شد</string>
|
||||
<string name="live_stream_live_tag">زنده</string>
|
||||
@@ -396,6 +403,7 @@
|
||||
<string name="are_you_sure_you_want_to_log_out">خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟</string>
|
||||
<string name="followed_tags">برچسب های دنبال شده</string>
|
||||
<string name="relay_setup">رله ها</string>
|
||||
<string name="discover_marketplace">بازار</string>
|
||||
<string name="discover_live">زنده</string>
|
||||
<string name="discover_community">انجمن</string>
|
||||
<string name="discover_chat">گپ</string>
|
||||
@@ -454,6 +462,7 @@
|
||||
<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_to_clipboard">کپی به کلیپبورد</string>
|
||||
<string name="copy_url_to_clipboard">کپی URL به کلیپبورد</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">کپی شناسه یادداشت به کلیپبورد</string>
|
||||
<string name="created_at">ایجاد شده در</string>
|
||||
@@ -489,6 +498,9 @@
|
||||
<string name="paid">پرداخت شد</string>
|
||||
<string name="wallet_number">کیف پول %1$s</string>
|
||||
<string name="error_opening_external_signer">خطا در بازکردن اپلیکیشن امضا کننده</string>
|
||||
<string name="error_opening_external_signer_description">اپ امضا کننده یافت نشد. بررسی کنید که اپ حذف نشده باشد.</string>
|
||||
<string name="sign_request_rejected">اپ امضا کننده رد کرد</string>
|
||||
<string name="sign_request_rejected_description">مطمئن شوید که اپ امضا کننده این تراکنش را تایید کرده است</string>
|
||||
<string name="no_wallet_found_with_error">هیچ کیف پولی برای پرداخت صورتحساب لایتنینگ یافت نشد (خطا: %1$s). لطفا کیف پول لایتنینگی برای استفاده از زپ نصب کنید</string>
|
||||
<string name="no_wallet_found">هیچ کیف پولی برای پرداخت صورتحساب لایتنینگ یافت نشد. لطفا یک کیف پول لایتنینگی برای استفاده از زپ نصب کنید</string>
|
||||
<string name="hidden_words">کلمات پنهان</string>
|
||||
@@ -506,6 +518,7 @@
|
||||
<string name="cashu_failed_redemption_explainer_already_spent">توکن Cashu خرج شده است.</string>
|
||||
<string name="cashu_successful_redemption">Cashu دریافت شد</string>
|
||||
<string name="cashu_successful_redemption_explainer">%1$s ساتوشی به کیف پول شما ارسال شد. (کارمزد: %2$s ساتوشی)</string>
|
||||
<string name="cashu_no_wallet_found">هیچ کیف پول Cashu سازگاری در سیستم یافت نشد</string>
|
||||
<string name="error_unable_to_fetch_invoice">صورتحساب از سرور گیرنده دریافت نشد</string>
|
||||
<string name="wallet_connect_pay_invoice_error_error">ارائه دهنده اتصال کیف پول شما خطای روبرو را داد: %1$s</string>
|
||||
<string name="could_not_connect_to_tor">نمی توان به Tor وصل شد</string>
|
||||
@@ -513,6 +526,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">LNUrl از آدرس لایتنینگ \"%1$s\" بدست نیامد. تنظیمات کاربر را بررسی کنید.</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">خدمات لایتنینگ %1$s گیرنده دردسترس نیست. از آدرس لایتنینگ \"%2$s\" محاسبه شد. خطا: %3$s. بررسی کنید آیا سرور کار می کند و آیا آدرس لایتنینگ صحیح است.</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">مشکل %1$s حل نشد. بررسی کنید که به اینترنت متصل باشید، سرور کار کند و آدرس لایتنینگ %2$s صحیح باشد.</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">مشکل %1$s حل نشد. بررسی کنید که به اینترنت متصل باشید، سرور کار کند و آدرس لایتنینگ %2$s صحیح باشد. \n\nخطای %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">صورتحساب از %1$s گرفته نشد</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">خطا در تفسیر JSON از آدرس لایتنینگ. تنظیمات لایتنینگ کاربر را بررسی کنید.</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">URL بازخوانی در پیکربندی سرور آدرس لایتنینگ یافت نشد</string>
|
||||
@@ -532,4 +546,49 @@
|
||||
<string name="push_server_install_app_description">برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Nfty](https://ntfy.sh/).
|
||||
پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید.
|
||||
</string>
|
||||
<string name="payment_required_title">پیغام از طرف %1$s</string>
|
||||
<string name="thread_title">رسته</string>
|
||||
<string name="send_the_seller_a_message">به فروشنده پیغام دهید</string>
|
||||
<string name="hi_seller_is_this_still_available">سلام %1$s، هنوز موجوده؟</string>
|
||||
<string name="hi_there_is_this_still_available">سلام، هنوز موجوده؟</string>
|
||||
<string name="classifieds">چیزی بفروشید</string>
|
||||
<string name="classifieds_title">عنوان</string>
|
||||
<string name="classifieds_title_placeholder">آیفون 13</string>
|
||||
<string name="classifieds_condition">شرایط</string>
|
||||
<string name="classifieds_category">دسته بندی</string>
|
||||
<string name="classifieds_price">قیمت (به ساتوشی)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">موقعیت مکانی</string>
|
||||
<string name="classifieds_location_placeholder">شهر، استان، کشور</string>
|
||||
<string name="classifieds_condition_new">جدید</string>
|
||||
<string name="classifieds_condition_new_explainer">این مورد نو هست، در بسته بندی اصلی</string>
|
||||
<string name="classifieds_condition_like_new">در حد نو</string>
|
||||
<string name="classifieds_condition_like_new_explainer">دست دوم است، اما ظاهرش تمیز است</string>
|
||||
<string name="classifieds_condition_good">خوب</string>
|
||||
<string name="classifieds_condition_good_explainer">کمی در ظاهر نشان می دهد که دست دوم است</string>
|
||||
<string name="classifieds_condition_fair">منصفانه</string>
|
||||
<string name="classifieds_condition_fair_explainer">هنوز مناسب است و کار می کند</string>
|
||||
<string name="classifieds_category_clothing">پوشاک</string>
|
||||
<string name="classifieds_category_accessories">لوازم جانبی</string>
|
||||
<string name="classifieds_category_electronics">الکترونیکی</string>
|
||||
<string name="classifieds_category_furniture">مبلمان</string>
|
||||
<string name="classifieds_category_collectibles">یادگاری</string>
|
||||
<string name="classifieds_category_books">کتاب</string>
|
||||
<string name="classifieds_category_pets">حیوانات خانگی</string>
|
||||
<string name="classifieds_category_sports">ورزش</string>
|
||||
<string name="classifieds_category_fitness">تناسب اندام</string>
|
||||
<string name="classifieds_category_art">هنر</string>
|
||||
<string name="classifieds_category_crafts">دست سازه</string>
|
||||
<string name="classifieds_category_home">خانه</string>
|
||||
<string name="classifieds_category_office">اداره</string>
|
||||
<string name="classifieds_category_food">خوراکی</string>
|
||||
<string name="classifieds_category_misc">گوناگون</string>
|
||||
<string name="classifieds_category_other">سایر</string>
|
||||
<string name="failed_to_upload_media_no_details">رسانه بارگذاری نشد</string>
|
||||
<string name="could_not_open_the_compressed_file">فایل فشرده باز نشد</string>
|
||||
<string name="error_when_compressing_media">خطا در فشرده سازی رسانه: %1$s</string>
|
||||
<string name="failed_to_upload_media">خطای بارگیری: %1$s</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">سرور پس از بارگذاری URL نداد</string>
|
||||
<string name="could_not_download_from_the_server">فایل بارگذاری شده از سرور بارگیری نشد</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">فایل محلی برای بارگذاری آماده نشد: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -185,12 +185,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Pointer vers le QR code</string>
|
||||
<string name="show_qr">Montrer le QR code</string>
|
||||
<string name="profile_image">Image de profil</string>
|
||||
<string name="your_profile_image">Votre Photo 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é inapproprié par</string>
|
||||
@@ -90,6 +91,7 @@
|
||||
<string name="add_a_relay">Ajouter un relais</string>
|
||||
<string name="display_name">Nom visible du public</string>
|
||||
<string name="my_display_name">Mon nom visible du public</string>
|
||||
<string name="my_awesome_name">Ostrich McGénial</string>
|
||||
<string name="welcome">Bienvenue Ostrich!</string>
|
||||
<string name="username">Nom d\'utilisateur</string>
|
||||
<string name="my_username">Mon nom d\'utilisateur</string>
|
||||
@@ -137,12 +139,15 @@
|
||||
<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="ncryptsec_password">mot de passe pour ouvrir la clé</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="invalid_key_with_message">Clé invalide : %1$s</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="password_is_required">Mot de passe requis</string>
|
||||
<string name="key_is_required">La clé est requise</string>
|
||||
<string name="name_is_required">Un nom est requis</string>
|
||||
<string name="login">Connexion</string>
|
||||
@@ -193,14 +198,19 @@
|
||||
<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é
|
||||
\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.
|
||||
<string name="account_backup_tips2_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 séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité.
|
||||
\n\n- Ne mettez **pas** votre clé secrète sur 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.
|
||||
\n- **Conservez** une sauvegarde sécurisée de votre clé secrète pour la récupération de compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe.
|
||||
\n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe.
|
||||
</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe.
|
||||
\n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé.
|
||||
</string>
|
||||
<string name="failed_to_encrypt_key">Échec du chiffrement de votre clé privée</string>
|
||||
<string name="secret_key_copied_to_clipboard">Clé secrète (nsec) copiée dans le presse-papiers</string>
|
||||
<string name="copy_my_secret_key">Copier ma clé secrète</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Chiffrer et copier ma clé secrète</string>
|
||||
<string name="biometric_authentication_failed">Échec de l\'authentification</string>
|
||||
<string name="biometric_authentication_failed_explainer">La biométrie n\'a pas pu authentifier le propriétaire de ce téléphone</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">La biométrie n\'a pas pu authentifier le propriétaire de ce téléphone. Erreur : %1$s</string>
|
||||
@@ -452,6 +462,7 @@
|
||||
<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_create_public_private_chat_desription">Nouveau Groupe Public ou Privé</string>
|
||||
<string name="messages_new_message">Privé</string>
|
||||
<string name="messages_new_message_to">À</string>
|
||||
<string name="messages_new_message_subject">Sujet</string>
|
||||
@@ -468,6 +479,7 @@
|
||||
<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_to_clipboard">Copier dans le presse-papiers</string>
|
||||
<string name="copy_npub_to_clipboard">Copier npub dans le presse-papiers</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>
|
||||
@@ -516,7 +528,10 @@
|
||||
<string name="error_dialog_pay_invoice_error">Impossible de payer la facture</string>
|
||||
<string name="error_dialog_pay_withdraw_error">Impossible de retirer</string>
|
||||
<string name="error_parsing_nip47_title">Impossible de configurer Wallet Connect</string>
|
||||
<string name="error_parsing_nip47">Erreur lors de l\'analyse de l\'événement de connexion NIP-47. Vérifiez si tout est correct avec le prestataire de votre Portefeuille : %1$s. Erreur : %2$s</string>
|
||||
<string name="error_parsing_nip47_no_error">Erreur lors de l\'analyse de l\'événement de connexion NIP-47. Vérifiez si tout est correct avec le prestataire de votre Portefeuille : %1$s.</string>
|
||||
<string name="cashu_failed_redemption">Impossible d\'utiliser ce Cashu</string>
|
||||
<string name="cashu_failed_redemption_explainer_error_msg">Le Mint a fourni le message d\'erreur suivant : %1$s</string>
|
||||
<string name="cashu_failed_redemption_explainer_already_spent">Les jetons de cashu sont déjà dépensés.</string>
|
||||
<string name="cashu_successful_redemption">Cashu reçu</string>
|
||||
<string name="cashu_successful_redemption_explainer">%1$s sats ont été envoyés à votre portefeuille. (Frais : %2$s sats)</string>
|
||||
@@ -528,6 +543,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Impossible d\'assembler LNUrl à partir de l\'adresse Lightning \"%1$s\". Vérifiez la configuration de l\'utilisateur</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Le service lightning du récepteur à %1$s n\'est pas disponible. Il a été calculé à partir de l\'adresse lightning \"%2$s\". Erreur: %3$s. Vérifiez si le serveur est en ligne et si l\'adresse lightning est correcte</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Impossible de résoudre %1$s. Vérifiez si vous êtes connecté, si le serveur est en ligne et si l\'adresse lightning %2$s est correcte</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Impossible de résoudre %1$s. Vérifiez si vous êtes connecté, si le serveur est actif et si l\'adresse Lightning %2$s est correcte.\n\nL\'erreur était : %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Impossible de récupérer la facture depuis %1$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Erreur lors de l\'analyse du JSON à partir de l\'Adresse Lightning. Vérifiez la configuration lightning de l\'utilisateur</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">URL de callback introuvable dans la configuration serveur de l\'adresse lightning de l\'utilisateur</string>
|
||||
@@ -592,4 +608,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Le serveur n\'a pas fourni d\'URL après le téléversement</string>
|
||||
<string name="could_not_download_from_the_server">Impossible de télécharger le média depuis le serveur</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Impossible de préparer le fichier local à téléverser: %1$s</string>
|
||||
<string name="login_with_qr_code">Se connecter avec un QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Accueil</string>
|
||||
<string name="route_search">Rechercher</string>
|
||||
<string name="route_discover">Découvrir</string>
|
||||
<string name="route_messages">Messages</string>
|
||||
<string name="route_notifications">Notifications</string>
|
||||
<string name="route_global">Général</string>
|
||||
<string name="route_video">Shorts</string>
|
||||
<string name="route_security_filters">Filtres de Sécurité</string>
|
||||
<string name="new_post">Nouveau Message</string>
|
||||
<string name="new_short">Nouveaux Shorts : images ou vidéos</string>
|
||||
<string name="new_community_note">Nouvelle Note Communautaire</string>
|
||||
<string name="open_all_reactions_to_this_post">Ouvrir toutes les réactions à ce message</string>
|
||||
<string name="close_all_reactions_to_this_post">Fermer toutes les réactions à ce message</string>
|
||||
<string name="reply_description">Répondre</string>
|
||||
<string name="boost_or_quote_description">Booster ou Citer</string>
|
||||
<string name="like_description">J\'aime</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">Photo de Profil de %1$s</string>
|
||||
<string name="relay_info">Relais %1$s</string>
|
||||
<string name="expand_relay_list">Développer la liste des relais</string>
|
||||
<string name="note_options">Options de la note</string>
|
||||
<string name="relay_list_selector">Sélecteur de liste de relais</string>
|
||||
<string name="poll">Sondage</string>
|
||||
<string name="disable_poll">Désactiver le Sondage</string>
|
||||
<string name="add_bitcoin_invoice">Facture Bitcoin</string>
|
||||
<string name="cancel_bitcoin_invoice">Annuler la Facture Bitcoin</string>
|
||||
<string name="cancel_classifieds">Annuler la Vente d\'un Objet</string>
|
||||
<string name="add_zapraiser">Collecte de Zaps</string>
|
||||
<string name="cancel_zapraiser">Annuler la Collecte de Zaps</string>
|
||||
<string name="add_location">Emplacement</string>
|
||||
<string name="remove_location">Retirer l\'Emplacement</string>
|
||||
<string name="add_zap_split">Partage des Zaps</string>
|
||||
<string name="cancel_zap_split">Annuler le partage des Zaps</string>
|
||||
<string name="add_content_warning">Ajouter un avertissement de contenu</string>
|
||||
<string name="remove_content_warning">Retirer l\'avertissement de contenu</string>
|
||||
<string name="show_npub_as_a_qr_code">Afficher npub en tant que QR code</string>
|
||||
<string name="invalid_nip19_uri">Adresse invalide</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst a reçu une URI à ouvrir mais cette URI était invalide : %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Mutass a QR kódra</string>
|
||||
<string name="show_qr">QR kód megjelenítése</string>
|
||||
<string name="profile_image">Profil kép</string>
|
||||
<string name="your_profile_image">Profilkép</string>
|
||||
<string name="scan_qr">QR kód beolvasása</string>
|
||||
<string name="show_anyway">Mutasd</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">A bejegyzést nem megfelelőként jelölte meg</string>
|
||||
@@ -194,12 +195,6 @@
|
||||
<string name="mark_all_new_as_read">Az összes új megjelölése olvasottként</string>
|
||||
<string name="mark_all_as_read">Összes megjelölése olvasottként</string>
|
||||
<string name="backup_keys">Biztonsági Kulcsok</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Kulcs- és biztonsági mentési tippek
|
||||
\n\nFiókját titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-el kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet.
|
||||
\n\n- **Ne** helyezze el titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik.
|
||||
\n- Az Amethyst fejlesztők a titkos kulcsodat **soha** nem fogják elkérni.
|
||||
\n- A fiók-helyreállításhoz, titkos kulcsáról **mindig** készítsen biztonságos biztonsági másolatot. Javasoljuk a jelszókezelő használatát.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">A titkos kulcs (nsec) a vágólapra másolva</string>
|
||||
<string name="copy_my_secret_key">A titkos kulcsom másolása</string>
|
||||
<string name="biometric_authentication_failed">Hitelesítés nem sikerült</string>
|
||||
@@ -453,6 +448,7 @@
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Az Amethystnek ennek a módnak az aktiválásához NIP-24 üzenetet kell küldenie (GiftWrapped, Zárolt direkt és csoportos üzeneteket). A NIP-24 új, és a legtöbb kliens még nem implementálta. Győződj meg arról, hogy a fogadó fél kompatibilis klienst használ.</string>
|
||||
<string name="new_feature_nip24_activate">Aktiválás</string>
|
||||
<string name="messages_create_public_chat">Publikus</string>
|
||||
<string name="messages_create_public_private_chat_desription">Új nyilvános vagy privát csoport</string>
|
||||
<string name="messages_new_message">Privát</string>
|
||||
<string name="messages_new_message_to">Címzett</string>
|
||||
<string name="messages_new_message_subject">Téma</string>
|
||||
@@ -469,6 +465,7 @@
|
||||
<string name="automatically_show_url_preview_description">URL előnézetek megjelenítése</string>
|
||||
<string name="load_image_description">Mikor kell a képeket betölteni</string>
|
||||
<string name="copy_to_clipboard">Másolás a vágólapra</string>
|
||||
<string name="copy_npub_to_clipboard">Npub másolása a vágólapra</string>
|
||||
<string name="copy_url_to_clipboard">Az URL vágólapra másolása</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">A bejegyzésazonosító vágólapra másolása</string>
|
||||
<string name="created_at">Létrehozva</string>
|
||||
@@ -532,6 +529,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Nem sikerült az LNUrl-t a(z) \"%1$s\" Lightning-címről összeállítani. Ellenőrizd a felhasználó beállítását</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">A fogadó lightning szolgáltatása itt: %1$s nem érhető el. A \"%2$s\" lightning címből lett kiszámítva. Hiba: %3$s. Ellenőrizd, hogy a szerver működik-e, és hogy a lightning cím helyes-e</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Nem sikerült megoldani: %1$s. Ellenőrizd, hogy csatlakozol-e, működik-e a szerver, és hogy a %2$s lightning cím helyes-e</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Nem sikerült megoldani: %1$s. Ellenőrizd, hogy csatlakozol-e, működik-e a szerver, és hogy a %2$s lightning cím helyes-e.\n\nKivétel: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Nem sikerült a számlát a következőtől: %1$s lekérni</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Hiba a Lightning-címből származó JSON elemzésekor. Ellenőrizd a felhasználó Lightning beállítását</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">A visszahívási URL a felhasználó Lightning címszerver konfigurációjában nem található</string>
|
||||
@@ -596,4 +594,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">A szerver a feltöltés után nem adott URL-t</string>
|
||||
<string name="could_not_download_from_the_server">Nem sikerült a szerverről a feltöltött médiát letölteni</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s</string>
|
||||
<string name="login_with_qr_code">Bejelentkezés QR kóddal</string>
|
||||
<string name="route">Útvonal</string>
|
||||
<string name="route_home">Főoldal</string>
|
||||
<string name="route_search">Keresés</string>
|
||||
<string name="route_discover">Felfedezés</string>
|
||||
<string name="route_messages">Üzenetek</string>
|
||||
<string name="route_notifications">Értesítések</string>
|
||||
<string name="route_global">Globális</string>
|
||||
<string name="route_video">Rövidfilmek</string>
|
||||
<string name="route_security_filters">Biztonsági szűrők</string>
|
||||
<string name="new_post">Új bejegyzés</string>
|
||||
<string name="new_short">Új rövidfilmek: képek vagy videók</string>
|
||||
<string name="new_community_note">Új közösségi bejegyzés</string>
|
||||
<string name="open_all_reactions_to_this_post">Nyissa meg a bejegyzésre adott összes reakciót</string>
|
||||
<string name="close_all_reactions_to_this_post">Zárja be a bejegyzésre adott összes reakciót</string>
|
||||
<string name="reply_description">Válasz</string>
|
||||
<string name="boost_or_quote_description">Megosztás vagy Idézés</string>
|
||||
<string name="like_description">Like</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">%1$s profilképe</string>
|
||||
<string name="relay_info">%1$s csomópont</string>
|
||||
<string name="expand_relay_list">Csomópont lista kibontása</string>
|
||||
<string name="note_options">Bejegyzés opciók</string>
|
||||
<string name="relay_list_selector">Csomópont lista választó</string>
|
||||
<string name="poll">Szavazás</string>
|
||||
<string name="disable_poll">Szavazás kikapcsolása</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoin számla</string>
|
||||
<string name="cancel_bitcoin_invoice">Bitcoin számla visszavonása</string>
|
||||
<string name="cancel_classifieds">Egy tétel eladásának visszavonása</string>
|
||||
<string name="add_zapraiser">ZapGyűjtés</string>
|
||||
<string name="cancel_zapraiser">ZapGyűjtés visszavonása</string>
|
||||
<string name="add_location">Pozíció</string>
|
||||
<string name="remove_location">Pozíció törlése</string>
|
||||
<string name="add_zap_split">Zap megosztások</string>
|
||||
<string name="cancel_zap_split">Zap megosztások visszavonása</string>
|
||||
<string name="add_content_warning">Tartalomra vonatkozó figyelmeztetés</string>
|
||||
<string name="remove_content_warning">Tartalomra vonatkozó figyelmeztetés visszavonása</string>
|
||||
<string name="show_npub_as_a_qr_code">Az npub megjelenítése QR-kódként</string>
|
||||
<string name="invalid_nip19_uri">Érvénytelen cím</string>
|
||||
<string name="invalid_nip19_uri_description">Az Amethyst kapott egy URI-t a megnyitáshoz, de az érvénytelen volt: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -185,12 +185,6 @@
|
||||
<string name="mark_all_new_as_read">Tandai semua yang baru sebagai telah dibaca</string>
|
||||
<string name="mark_all_as_read">Tandai semua sebagai telah dibaca</string>
|
||||
<string name="backup_keys">Kunci Cadangan</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Kiat Pencadangan dan Keamanan Kunci
|
||||
\n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini berupa string acak panjang yang dimulai dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia Anda dapat mempublikasikan konten menggunakan identitas Anda.
|
||||
\n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau aplikasi apa pun yang tidak Anda percayai.
|
||||
\n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda.
|
||||
\n- **Simpanlah** cadangan kunci rahasia Anda dengan aman untuk pemulihan akun. Kami sarankan menggunakan aplikasi pengelola kata sandi.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Kunci rahasia (nsec) telah disalin ke papan klip</string>
|
||||
<string name="copy_my_secret_key">Salin kunci rahasia saya</string>
|
||||
<string name="biometric_authentication_failed">Autentikasi gagal</string>
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
<string name="mark_all_new_as_read">Tandai pesan baru telah dibaca</string>
|
||||
<string name="mark_all_as_read">Tandai semua telah dibaca</string>
|
||||
<string name="backup_keys">Cadangkan Kunci</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">"\n ## Tip Pencadangan dan Keamanan Utama\n \n\nAkun Anda diamankan dengan kunci rahasia. Kuncinya adalah string acak panjang yang dimulai dengan **nsec1**.. Siapa pun yang memiliki akses ke kunci rahasia Anda dapat mempublikasikan konten menggunakan identitas Anda.\n \n\n- **Jangan** letakkan kunci rahasia Anda di situs web atau perangkat lunak apa pun yang tidak Anda percayai.\n \n- Pengembang Aplikasi Amethyst **tidak akan pernah** meminta kunci rahasia Anda.\n \n- **Pastikan** simpan cadangan aman kunci rahasia Anda untuk pemulihan akun. Kami merekomendasikan menggunakan aplikasi khusus pengelola kata sandi.\n "</string>
|
||||
<string name="secret_key_copied_to_clipboard">Kunci rahasia (nsec) disalin ke clipboard</string>
|
||||
<string name="copy_my_secret_key">Salin kunci rahasia saya</string>
|
||||
<string name="biometric_authentication_failed">Autentikasi gagal</string>
|
||||
|
||||
@@ -182,12 +182,6 @@
|
||||
<string name="mark_all_new_as_read">Segna tutti i nuovi come letti</string>
|
||||
<string name="mark_all_as_read">Segna tutti come letti</string>
|
||||
<string name="backup_keys">Esegui backup delle chiavi</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Backup delle Chiavi e Consigli di Sicurezza
|
||||
\n\nIl tuo account è reso sicuro da una chiave privata. La chiave è una lunga stringa di caratteri casuali che inizia con **nsec1**. Chiunque abbia accesso alla tua chiave privata puó pubblicare contenuti con la tua identitá.
|
||||
\n\n- Non devi **mai** inserire la tua chiave privata in un sito o software di cui non ti fidi.
|
||||
\n- Gli sviluppatori di Amethyst non chiederanno **mai** la tua chiave privata.
|
||||
\n- Mantieni **sempre** al sicuro un backup della tua chiave privata per recuperare l\'account. Ti suggeriamo di usare un password manager.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Chiave segreta (nsec) copiata negli appunti</string>
|
||||
<string name="copy_my_secret_key">Copia la mia chiave segreta</string>
|
||||
<string name="biometric_authentication_failed">Autenticazione fallita</string>
|
||||
|
||||
@@ -177,12 +177,6 @@
|
||||
<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"> ## 鍵のバックアップと安全に利用するためのTips
|
||||
\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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Richt camera op de QR-code</string>
|
||||
<string name="show_qr">Toon QR</string>
|
||||
<string name="profile_image">Profielfoto</string>
|
||||
<string name="your_profile_image">Jouw profielfoto</string>
|
||||
<string name="scan_qr">Scan QR</string>
|
||||
<string name="show_anyway">Laat toch zien</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Bericht gemarkeerd als ongepast door</string>
|
||||
@@ -194,12 +195,6 @@
|
||||
<string name="mark_all_new_as_read">Nieuwe markeren als gelezen</string>
|
||||
<string name="mark_all_as_read">Alles markeren als gelezen</string>
|
||||
<string name="backup_keys">Back-up sleutels</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Sleutel back-up en veiligheidstips
|
||||
\n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit.
|
||||
\n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt.
|
||||
\n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen.
|
||||
\n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Privésleutel (nsec) gekopieerd naar klembord</string>
|
||||
<string name="copy_my_secret_key">Kopieer mijn privésleutel</string>
|
||||
<string name="biometric_authentication_failed">Authenticatie mislukt</string>
|
||||
@@ -453,6 +448,7 @@
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Voor het activeren van deze modus is Amethyst nodig om een NIP-24 bericht te versturen. NIP-24 is nieuw en de meeste clients hebben deze nog niet geïmplementeerd. Zorg ervoor dat de ontvanger een compatibele client gebruikt.</string>
|
||||
<string name="new_feature_nip24_activate">Activeren</string>
|
||||
<string name="messages_create_public_chat">Publiek</string>
|
||||
<string name="messages_create_public_private_chat_desription">Nieuwe openbare of besloten groep</string>
|
||||
<string name="messages_new_message">Privé</string>
|
||||
<string name="messages_new_message_to">Aan</string>
|
||||
<string name="messages_new_message_subject">Onderwerp</string>
|
||||
@@ -469,6 +465,7 @@
|
||||
<string name="automatically_show_url_preview_description">Toon URL-voorbeelden</string>
|
||||
<string name="load_image_description">Wanneer afbeeldingen te laden</string>
|
||||
<string name="copy_to_clipboard">Kopiëren naar klembord</string>
|
||||
<string name="copy_npub_to_clipboard">Kopieer npub naar klembord</string>
|
||||
<string name="copy_url_to_clipboard">Kopieer URL naar klembord</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Note naar klembord kopiëren</string>
|
||||
<string name="created_at">Gemaakt op</string>
|
||||
@@ -597,4 +594,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Server heeft geen URL opgegeven na uploaden</string>
|
||||
<string name="could_not_download_from_the_server">Kan de geüploade media niet downloaden van de server</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Kon lokaal bestand niet voorbereiden voor upload: %1$s</string>
|
||||
<string name="login_with_qr_code">Inloggen met QR-Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Beginscherm</string>
|
||||
<string name="route_search">Zoeken</string>
|
||||
<string name="route_discover">Ontdekken</string>
|
||||
<string name="route_messages">Berichten</string>
|
||||
<string name="route_notifications">Notificaties</string>
|
||||
<string name="route_global">Globaal</string>
|
||||
<string name="route_video">Shorts</string>
|
||||
<string name="route_security_filters">Beveiligingsfilters</string>
|
||||
<string name="new_post">Nieuw bericht</string>
|
||||
<string name="new_short">Nieuwe shorts: afbeeldingen of video\'s</string>
|
||||
<string name="new_community_note">Nieuwe community note</string>
|
||||
<string name="open_all_reactions_to_this_post">Open alle reacties op dit bericht</string>
|
||||
<string name="close_all_reactions_to_this_post">Sluit alle reacties op dit bericht</string>
|
||||
<string name="reply_description">Antwoorden</string>
|
||||
<string name="boost_or_quote_description">Boost of citaat</string>
|
||||
<string name="like_description">Vind ik leuk</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">Profielfoto van %1$s</string>
|
||||
<string name="relay_info">Relay %1$s</string>
|
||||
<string name="expand_relay_list">Lijst van relays uitvouwen</string>
|
||||
<string name="note_options">Note opties</string>
|
||||
<string name="relay_list_selector">Relay selectiemenu</string>
|
||||
<string name="poll">Poll</string>
|
||||
<string name="disable_poll">Poll uitschakelen</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoin invoice</string>
|
||||
<string name="cancel_bitcoin_invoice">Annuleer Bitcoin invoice</string>
|
||||
<string name="cancel_classifieds">Verkoop van een item annuleren</string>
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">Annuleer Zapraiser</string>
|
||||
<string name="add_location">Locatie</string>
|
||||
<string name="remove_location">Locatie verwijderen</string>
|
||||
<string name="add_zap_split">Zap splits</string>
|
||||
<string name="cancel_zap_split">Annuleer splitsing Zap</string>
|
||||
<string name="add_content_warning">Content waarschuwing toevoegen</string>
|
||||
<string name="remove_content_warning">Content waarschuwing verwijderen</string>
|
||||
<string name="show_npub_as_a_qr_code">Toon npub als een QR-code</string>
|
||||
<string name="invalid_nip19_uri">Ongeldig adres</string>
|
||||
<string name="invalid_nip19_uri_description">Amethist ontving een URI om te openen, maar die uri was ongeldig: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<string name="point_to_the_qr_code">Aponte para o código QR</string>
|
||||
<string name="show_qr">Mostrar QR</string>
|
||||
<string name="profile_image">Imagem de perfil</string>
|
||||
<string name="scan_qr">Escanear QR</string>
|
||||
<string name="your_profile_image">Sua Foto de Perfil</string>
|
||||
<string name="scan_qr">Ler QR</string>
|
||||
<string name="show_anyway">Mostrar de qualquer maneira</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">A postagem foi sinalizada como imprópria por</string>
|
||||
<string name="post_not_found">postagem não encontrada</string>
|
||||
@@ -69,7 +70,7 @@
|
||||
<string name="new_channel">Novo Canal</string>
|
||||
<string name="channel_name">Nome do Canal</string>
|
||||
<string name="my_awesome_group">Meu grupo</string>
|
||||
<string name="picture_url">Url da foto</string>
|
||||
<string name="picture_url">URL da foto</string>
|
||||
<string name="description">Descrição</string>
|
||||
<string name="about_us">"Sobre nós.. "</string>
|
||||
<string name="what_s_on_your_mind">O que você está pensando?</string>
|
||||
@@ -103,7 +104,7 @@
|
||||
<string name="image_saved_to_the_gallery">Imagem salva para a galeria</string>
|
||||
<string name="failed_to_save_the_image">Falha ao salvar a imagem</string>
|
||||
<string name="upload_image">Enviar Imagem</string>
|
||||
<string name="uploading">Enviando...</string>
|
||||
<string name="uploading">Enviando…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Usuário não tem um endereço lightning configurado para receber sats</string>
|
||||
<string name="reply_here">"responda aqui.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copia o ID do canal (note) para compartilhar</string>
|
||||
@@ -138,12 +139,15 @@
|
||||
<string name="clear">Limpar</string>
|
||||
<string name="app_logo">Logo do aplicativo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex chave privada</string>
|
||||
<string name="ncryptsec_password">senha para abrir a chave</string>
|
||||
<string name="show_password">Mostrar senha</string>
|
||||
<string name="hide_password">Esconder senha</string>
|
||||
<string name="invalid_key">Chave inválida</string>
|
||||
<string name="invalid_key_with_message">Chave inválida: %1$s</string>
|
||||
<string name="i_accept_the">"Eu aceito os "</string>
|
||||
<string name="terms_of_use">termos de uso</string>
|
||||
<string name="acceptance_of_terms_is_required">É necessário aceitar os termos de uso</string>
|
||||
<string name="password_is_required">Senha é necessária</string>
|
||||
<string name="key_is_required">Chave é obrigatória</string>
|
||||
<string name="name_is_required">Um nome é necessário</string>
|
||||
<string name="login">Entrar</string>
|
||||
@@ -194,14 +198,17 @@
|
||||
<string name="mark_all_new_as_read">Marcar todas novas solicitações como lidas</string>
|
||||
<string name="mark_all_as_read">Marcar todas como lidas</string>
|
||||
<string name="backup_keys">Copia de segurança das chaves</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Backup de chaves e dicas de segurança
|
||||
\n\nSua conta é protegida por uma chave secreta. A chave é uma string aleatória longa começando com **nsec1**. Qualquer pessoa que tenha acesso à sua chave secreta pode publicar conteúdo usando sua identidade.
|
||||
\n\n- **Não** coloque sua chave secreta em qualquer site ou software em que não confie.
|
||||
\n- Os desenvolvedores do Amethyst **nunca** pedirão sua chave secreta.
|
||||
\n- **Faça** um backup seguro de sua chave secreta para recuperação de conta. Recomendamos o uso de um gerenciador de senhas.
|
||||
</string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos"> ## Dicas de Backup de Chave e Segurança
|
||||
\n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade.
|
||||
\n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie.
|
||||
\n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta.
|
||||
\n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas.</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha.
|
||||
\n\nSe você perder sua senha, não será possível recuperar sua chave.</string>
|
||||
<string name="failed_to_encrypt_key">Falha ao criptografar sua chave privada</string>
|
||||
<string name="secret_key_copied_to_clipboard">Chave secreta (nsec) copiada</string>
|
||||
<string name="copy_my_secret_key">Copiar minha chave secreta</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Criptografar e copiar minha chave secreta</string>
|
||||
<string name="biometric_authentication_failed">Autenticação falhou</string>
|
||||
<string name="biometric_authentication_failed_explainer">A autenticação biométrica falhou ao autenticar o proprietário deste telefone</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">A autenticação biométrica falhou ao autenticar o proprietário deste telefone. Erro: %1$s</string>
|
||||
@@ -274,7 +281,7 @@
|
||||
<string name="post_poll">Postar enquete</string>
|
||||
<string name="poll_heading_required">Campos obrigatórios:</string>
|
||||
<string name="poll_zap_recipients">Destinatários Zap</string>
|
||||
<string name="poll_primary_description">Descrição da enquete principal...</string>
|
||||
<string name="poll_primary_description">Descrição da enquete principal…</string>
|
||||
<string name="poll_option_index">Opção %s</string>
|
||||
<string name="poll_option_description">Descrição da opção de enquete</string>
|
||||
<string name="poll_heading_optional">Campos opcionais:</string>
|
||||
@@ -321,7 +328,7 @@
|
||||
<string name="connect_via_tor_short">Configuração do Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Conecte-se através da configuração do Orbot</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Desconectar do Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Seus dados serão transferidos imediatamente na rede regular</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Seus dados serão transferidos imediatamente na rede aberta</string>
|
||||
<string name="yes">Sim</string>
|
||||
<string name="no">Não</string>
|
||||
<string name="follow_list_selection">Lista de seguidores</string>
|
||||
@@ -447,12 +454,13 @@
|
||||
<string name="zap_forward_title">Encaminhar Zaps para:</string>
|
||||
<string name="zap_forward_explainer">Os clientes que suportam encaminharão zaps para o endereço lightning ou perfil de usuário abaixo, em vez do seu</string>
|
||||
<string name="geohash_title">Expor localização como </string>
|
||||
<string name="geohash_explainer">Adiciona um Geohash da sua localização à postagem. O público saberá que você está a 5 km (3 milhas) do local atual</string>
|
||||
<string name="geohash_explainer">Adicione um geohash da sua localização à postagem. O público saberá que você está a 5 km (3 milhas) do local atual</string>
|
||||
<string name="add_sensitive_content_explainer">Adiciona aviso de conteúdo sensível antes de mostrar seu conteúdo. Isso é ideal para qualquer conteúdo NSFW ou conteúdo que algumas pessoas possam considerar ofensivo ou perturbador</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Novo recurso</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Ativando este modo requer o Amethyst para enviar uma mensagem de NIP-24 (GiftWrapped, Sealed Direct and Group Messages). NIP-24 é novo e a maioria dos clientes ainda não o implementaram. Certifique-se de que o destinatário está usando um cliente compatível.</string>
|
||||
<string name="new_feature_nip24_activate">Ativar</string>
|
||||
<string name="messages_create_public_chat">Público</string>
|
||||
<string name="messages_create_public_private_chat_desription">Novo Grupo Público ou Privado</string>
|
||||
<string name="messages_new_message">Privado</string>
|
||||
<string name="messages_new_message_to">Para</string>
|
||||
<string name="messages_new_message_subject">Assunto</string>
|
||||
@@ -463,18 +471,19 @@
|
||||
<string name="messages_new_subject_message_placeholder">Mudando o nome dos novos objetivos.</string>
|
||||
<string name="paste_from_clipboard">Colar da área de transferência</string>
|
||||
<string name="language_description">Para a interface do aplicativo</string>
|
||||
<string name="theme_description">Tema Escuro, Claro ou Sistema</string>
|
||||
<string name="theme_description">Tema Escuro, Claro ou Padrão</string>
|
||||
<string name="automatically_load_images_gifs_description">Carregar automaticamente imagens e GIFs</string>
|
||||
<string name="automatically_play_videos_description">Reproduz automaticamente vídeos e GIFs</string>
|
||||
<string name="automatically_show_url_preview_description">Mostrar visualizações de URL</string>
|
||||
<string name="load_image_description">Quando carregar imagens</string>
|
||||
<string name="copy_to_clipboard">Copiar para a Área de Transferência</string>
|
||||
<string name="copy_npub_to_clipboard">Copiar npub para a área de transferência</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL para a área de transferência</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID da nota para a área de transferência</string>
|
||||
<string name="created_at">Criado em</string>
|
||||
<string name="rules">Regras</string>
|
||||
<string name="login_with_external_signer">Login com Amber</string>
|
||||
<string name="status_update">Atualize seu status</string>
|
||||
<string name="status_update">O que você está fazendo?</string>
|
||||
<string name="lightning_wallets_not_found">Erro ao analisar mensagem de erro</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Os votos são ponderados pelo valor do zap. Você pode definir um valor mínimo para evitar spammers e um valor máximo para evitar que grandes zappers assumam o controle da enquete. Use o mesmo valor em ambos os campos para garantir que cada voto tenha o mesmo valor. Deixe em branco para aceitar qualquer valor.</string>
|
||||
<string name="error_dialog_zap_error">Não foi possível enviar o Zap</string>
|
||||
@@ -597,4 +606,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">O servidor não forneceu uma URL após o upload</string>
|
||||
<string name="could_not_download_from_the_server">Não foi possível baixar o arquivo de mídia carregado do servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Não foi possível preparar o arquivo local para enviar: %1$s</string>
|
||||
<string name="login_with_qr_code">Entrar com Código QR</string>
|
||||
<string name="route">Rota</string>
|
||||
<string name="route_home">Início</string>
|
||||
<string name="route_search">Buscar</string>
|
||||
<string name="route_discover">Descobrir</string>
|
||||
<string name="route_messages">Mensagens</string>
|
||||
<string name="route_notifications">Notificações</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Vídeos Curtos</string>
|
||||
<string name="route_security_filters">Filtros de Segurança</string>
|
||||
<string name="new_post">Novo Post</string>
|
||||
<string name="new_short">Novos Vídeos Curtos: imagens ou vídeos</string>
|
||||
<string name="new_community_note">Nova Nota da Comunidade</string>
|
||||
<string name="open_all_reactions_to_this_post">Abrir todas as reações a esta postagem</string>
|
||||
<string name="close_all_reactions_to_this_post">Fechar todas as reações a esta postagem</string>
|
||||
<string name="reply_description">Responder</string>
|
||||
<string name="boost_or_quote_description">Impulsionar ou Citar</string>
|
||||
<string name="like_description">Gostar</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">Foto de perfil de %1$s</string>
|
||||
<string name="relay_info">Repassar %1$s</string>
|
||||
<string name="expand_relay_list">Expandir lista de repasse</string>
|
||||
<string name="note_options">Opções de nota</string>
|
||||
<string name="relay_list_selector">Seletor de lista de repasse</string>
|
||||
<string name="poll">Enquete</string>
|
||||
<string name="disable_poll">Desativar enquete</string>
|
||||
<string name="add_bitcoin_invoice">Fatura de Bitcoin</string>
|
||||
<string name="cancel_bitcoin_invoice">Cancelar Fatura de Bitcoin</string>
|
||||
<string name="cancel_classifieds">Cancelar Venda de Item</string>
|
||||
<string name="add_zapraiser">Avaliador Zap</string>
|
||||
<string name="cancel_zapraiser">Cancelar Avaliador Zap</string>
|
||||
<string name="add_location">Localização</string>
|
||||
<string name="remove_location">Remover Localização</string>
|
||||
<string name="add_zap_split">Divisão Zap</string>
|
||||
<string name="cancel_zap_split">Cancelar Divisão Zap</string>
|
||||
<string name="add_content_warning">Adicionar aviso de conteúdo</string>
|
||||
<string name="remove_content_warning">Remover aviso de conteúdo</string>
|
||||
<string name="show_npub_as_a_qr_code">Mostrar npub como um código QR</string>
|
||||
<string name="invalid_nip19_uri">Endereço inválido</string>
|
||||
<string name="invalid_nip19_uri_description">O Amethyst recebeu um URI para abrir, mas esse URI era inválido: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -183,12 +183,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">Peka mot QR-koden</string>
|
||||
<string name="show_qr">Visa QR</string>
|
||||
<string name="profile_image">Profilbild</string>
|
||||
<string name="your_profile_image">Din profilbild</string>
|
||||
<string name="scan_qr">Scanna QR</string>
|
||||
<string name="show_anyway">Visa ändå</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Inlägget flaggades som olämpligt av</string>
|
||||
@@ -138,12 +139,15 @@
|
||||
<string name="clear">Rensa</string>
|
||||
<string name="app_logo">App Logo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex privat nyckel</string>
|
||||
<string name="ncryptsec_password">lösenord för att öppna nyckeln</string>
|
||||
<string name="show_password">Visa lösenord</string>
|
||||
<string name="hide_password">Göm lösenord</string>
|
||||
<string name="invalid_key">Ogiltig nyckel</string>
|
||||
<string name="invalid_key_with_message">Ogiltig nyckel: %1$s</string>
|
||||
<string name="i_accept_the">"Jag accepterar "</string>
|
||||
<string name="terms_of_use">villkor</string>
|
||||
<string name="acceptance_of_terms_is_required">Godkännande av villkor krävs</string>
|
||||
<string name="password_is_required">Lösenord krävs</string>
|
||||
<string name="key_is_required">Nyckel krävs</string>
|
||||
<string name="name_is_required">Namn är obligatoriskt</string>
|
||||
<string name="login">Inloggning</string>
|
||||
@@ -194,13 +198,17 @@
|
||||
<string name="mark_all_new_as_read">Markera alla nya som lästa</string>
|
||||
<string name="mark_all_as_read">Markera allt som läst</string>
|
||||
<string name="backup_keys">Backup-nycklar</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Nyckelsäkerhetskopiering och säkerhetstips
|
||||
\n\nDitt konto är skyddat av en hemlig nyckel. Nyckeln är en lång slumpmässig sträng som börjar med **nsec1**. Alla som har tillgång till din hemliga nyckel kan publicera innehåll med din identitet.
|
||||
\n\n- Lägg **inte** in din hemliga nyckel på någon webbplats eller programvara som du inte litar på.
|
||||
\n- Amethyst-utvecklare kommer **aldrig** att be dig om din hemliga nyckel.
|
||||
\n- **Behåll en säker säkerhetskopia av din hemliga nyckel för kontoåterställning. Vi rekommenderar att du använder en lösenordshanterare. </string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos"> ## Nyckelsäkerhetsråd
|
||||
\n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet.
|
||||
\n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på.
|
||||
\n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel.
|
||||
\n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare.</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord.
|
||||
\n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel.</string>
|
||||
<string name="failed_to_encrypt_key">Misslyckades med att kryptera din privata nyckel</string>
|
||||
<string name="secret_key_copied_to_clipboard">Hemlig nyckel (nsec) kopierad till urklipp</string>
|
||||
<string name="copy_my_secret_key">Kopiera min hemliga nyckel</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Kryptera och kopiera min hemliga nyckel</string>
|
||||
<string name="biometric_authentication_failed">Autentisering misslyckades</string>
|
||||
<string name="biometric_authentication_failed_explainer">Biometri misslyckades med att autentisera ägaren av denna telefon</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">Biometri misslyckades med att autentisera ägaren av denna telefon. Fel: %1$s</string>
|
||||
@@ -451,6 +459,7 @@
|
||||
<string name="new_feature_nip24_might_not_be_available_description">För att aktivera denna funktion kräver det att Amethyst skickar ett NIP-24 meddelande (GiftWrapped, Förseglade Direkta och Gruppmeddelanden). NIP-24 är nytt och de flesta klienter har ännu inte implementerat det. Se till att mottagaren använder en kompatibel klient.</string>
|
||||
<string name="new_feature_nip24_activate">Aktivera</string>
|
||||
<string name="messages_create_public_chat">Publik</string>
|
||||
<string name="messages_create_public_private_chat_desription">Ny offentlig eller privat grupp</string>
|
||||
<string name="messages_new_message">Privat</string>
|
||||
<string name="messages_new_message_to">Till</string>
|
||||
<string name="messages_new_message_subject">Ämne</string>
|
||||
@@ -467,6 +476,7 @@
|
||||
<string name="automatically_show_url_preview_description">Visa förhandsgranskning av URL</string>
|
||||
<string name="load_image_description">När bilder ska laddas</string>
|
||||
<string name="copy_to_clipboard">Kopiera till Urklipp</string>
|
||||
<string name="copy_npub_to_clipboard">Kopiera npub till urklipp</string>
|
||||
<string name="copy_url_to_clipboard">Kopiera URL till urklipp</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Kopiera anteckningens ID till urklipp</string>
|
||||
<string name="created_at">Skapad den</string>
|
||||
@@ -595,4 +605,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Servern gav inte en URL efter uppladdning</string>
|
||||
<string name="could_not_download_from_the_server">Kunde inte ladda ner uppladdade medier från servern</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Kunde inte förbereda lokal fil att ladda upp: %1$s</string>
|
||||
<string name="login_with_qr_code">Logga in med QR-kod</string>
|
||||
<string name="route">Rutt</string>
|
||||
<string name="route_home">Hem</string>
|
||||
<string name="route_search">Sök</string>
|
||||
<string name="route_discover">Upptäck</string>
|
||||
<string name="route_messages">Meddelanden</string>
|
||||
<string name="route_notifications">Aviseringar</string>
|
||||
<string name="route_global">Globalt</string>
|
||||
<string name="route_video">Kortfilmer</string>
|
||||
<string name="route_security_filters">Säkerhetsfilter</string>
|
||||
<string name="new_post">Nytt inlägg</string>
|
||||
<string name="new_short">Nya kort: bilder eller videor</string>
|
||||
<string name="new_community_note">Nytt Community-meddelande</string>
|
||||
<string name="open_all_reactions_to_this_post">Öppna alla reaktioner på detta inlägg</string>
|
||||
<string name="close_all_reactions_to_this_post">Stäng alla reaktioner på detta inlägg</string>
|
||||
<string name="reply_description">Svara</string>
|
||||
<string name="boost_or_quote_description">Boosta eller citera</string>
|
||||
<string name="like_description">Gilla</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="profile_image_of_user">%1$s Profilbild</string>
|
||||
<string name="relay_info">Relä %1$s</string>
|
||||
<string name="expand_relay_list">Expandera relälistan</string>
|
||||
<string name="note_options">Notalternativ</string>
|
||||
<string name="relay_list_selector">Relälistväljare</string>
|
||||
<string name="poll">Omröstning</string>
|
||||
<string name="disable_poll">Inaktivera omröstning</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoin-faktura</string>
|
||||
<string name="cancel_bitcoin_invoice">Avbryt Bitcoin-faktura</string>
|
||||
<string name="cancel_classifieds">Avbryt Sälja en artikel</string>
|
||||
<string name="add_zapraiser">Zappraiser</string>
|
||||
<string name="cancel_zapraiser">Avbryt Zappraiser</string>
|
||||
<string name="add_location">Plats</string>
|
||||
<string name="remove_location">Ta bort plats</string>
|
||||
<string name="add_zap_split">Zap-split</string>
|
||||
<string name="cancel_zap_split">Avbryt Zap-split</string>
|
||||
<string name="add_content_warning">Lägg till varning för innehåll</string>
|
||||
<string name="remove_content_warning">Ta bort varning för innehåll</string>
|
||||
<string name="show_npub_as_a_qr_code">Visa npub som en QR-kod</string>
|
||||
<string name="invalid_nip19_uri">Ogiltig adress</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst fick en URI att öppna men den URI var ogiltig: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -182,12 +182,6 @@
|
||||
<string name="mark_all_new_as_read">Funga kama Mpya</string>
|
||||
<string name="mark_all_as_read">Funga kama Zimejulikana</string>
|
||||
<string name="backup_keys">Nakili Nakala za Ufunguo</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos"> ## Ufundi wa Nakala ya Ufunguo na Usalama
|
||||
\n\nAkaunti yako inalindwa na ufunguo wa siri. Ufunguo ni mfululizo mrefu wa herufi zisizo na mpangilio, ukitangulia na **nsec1**. Mtu yeyote aliye na ufikiaji wa ufunguo wako wa siri anaweza kuchapisha maudhui kwa kutumia kitambulisho chako.
|
||||
\n\n- **Usiweke** ufunguo wako wa siri kwenye tovuti au programu usio na imani.
|
||||
\n- Watengenezaji wa Amethyst **hawata** kuuliza ufunguo wako wa siri.
|
||||
\n- **Tunashauri** kuwa na nakala rudufu salama ya ufunguo wako wa siri kwa ajili ya kupata akaunti. Tunapendekeza kutumia meneja wa nywila.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Ufunguo wa siri (nsec) umeinakiliwa kwenye ubao wa kunakili</string>
|
||||
<string name="copy_my_secret_key">Nakili Ufunguo Wangu wa Siri</string>
|
||||
<string name="biometric_authentication_failed">Uthibitishaji umeshindwa</string>
|
||||
|
||||
@@ -171,12 +171,6 @@
|
||||
<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- அமேதிஸ்ட் டெவலப்பர்கள் உங்கள் ரகசியசாவியை உங்களிடம் எப்போதும் கேட்க மாட்டார்கள்.
|
||||
\ n- கணக்கு மீட்டெடுப்பதற்கு உங்கள் ரகசியசாவியின் பாதுகாப்பான காப்புப்பிரதியை **தவறாமல்** வைத்திருங்கள். கடவுச்சொல் நிர்வாகியைப் பயன்படுத்த பரிந்துரைக்கிறோம்.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">ரகசிய சாவி (nsec) கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது</string>
|
||||
<string name="copy_my_secret_key">எனது ரகசிய சாவியை நகலெடுக்கவும்</string>
|
||||
<string name="biometric_authentication_failed">அங்கீகரிப்பு தோல்வியுற்றது</string>
|
||||
|
||||
@@ -185,12 +185,6 @@
|
||||
<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 บัญชีของคุณมีความปลอดภัยด้วยรหัสลับ key คือสตริงสุ่มยาวที่ขึ้นต้นด้วย **nsec1** ทุกคนที่มีสิทธิ์เข้าถึงรหัสลับของคุณสามารถเผยแพร่เนื้อหาโดยใช้บัญชีของคุณได้
|
||||
\n\n- **อย่า** ใส่รหัสลับของคุณในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่เชื่อถือ
|
||||
\n- นักพัฒนา Amethyst จะ **ไม่เคย** ขอรหัสลับของคุณ
|
||||
\n- **เก็บ** สําเนาสํารองคีย์ลับของคุณไว้อย่างปลอดภัยสําหรับการกู้คืนบัญชี เราขอแนะนําให้ใช้ password manager.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">คัดลอก Secret key (nsec) ลงคลิปบอร์ด</string>
|
||||
<string name="copy_my_secret_key">คัดลอก secret key ของฉัน</string>
|
||||
<string name="biometric_authentication_failed"> เกิดข้อผิดพลาดในการตรวจสอบ</string>
|
||||
|
||||
@@ -191,12 +191,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">对准二维码</string>
|
||||
<string name="show_qr">显示二维码</string>
|
||||
<string name="profile_image">头像图片</string>
|
||||
<string name="your_profile_image">头像图片</string>
|
||||
<string name="scan_qr">扫描二维码</string>
|
||||
<string name="show_anyway">仍然显示</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">帖文被标记为不当</string>
|
||||
@@ -90,6 +91,8 @@
|
||||
<string name="add_a_relay">添加中继器</string>
|
||||
<string name="display_name">显示名称</string>
|
||||
<string name="my_display_name">我的显示名称</string>
|
||||
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||
<string name="welcome">欢迎!</string>
|
||||
<string name="username">用户名</string>
|
||||
<string name="my_username">我的用户名</string>
|
||||
<string name="about_me">关于我</string>
|
||||
@@ -143,7 +146,14 @@
|
||||
<string name="terms_of_use">使用条款</string>
|
||||
<string name="acceptance_of_terms_is_required">需要接受条款</string>
|
||||
<string name="key_is_required">需要密钥</string>
|
||||
<string name="name_is_required">名称必填</string>
|
||||
<string name="login">登录</string>
|
||||
<string name="sign_up">注册</string>
|
||||
<string name="create_account">创建帐户</string>
|
||||
<string name="how_should_we_call_you">我们应该如何称呼你?</string>
|
||||
<string name="don_t_have_an_account">还没有 Nostr 帐户?</string>
|
||||
<string name="already_have_an_account">已经有 Nostr 帐户?</string>
|
||||
<string name="create_a_new_account">创建新帐户</string>
|
||||
<string name="generate_a_new_key">生成新密钥</string>
|
||||
<string name="loading_feed">正在加载</string>
|
||||
<string name="loading_account">正在载入帐户</string>
|
||||
@@ -185,11 +195,6 @@
|
||||
<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>
|
||||
@@ -443,6 +448,7 @@
|
||||
<string name="new_feature_nip24_might_not_be_available_description">启用此模式需要 Amethyst 发送一条 NIP-24 消息(包装的、密封的私信和群聊消息)。因为 NIP-24 是新的,大多数客户端尚未执行。请确保接收方正在使用兼容的客户端。</string>
|
||||
<string name="new_feature_nip24_activate">启用</string>
|
||||
<string name="messages_create_public_chat">公开</string>
|
||||
<string name="messages_create_public_private_chat_desription">新建公开或私人群组</string>
|
||||
<string name="messages_new_message">私人</string>
|
||||
<string name="messages_new_message_to">至</string>
|
||||
<string name="messages_new_message_subject">主题</string>
|
||||
@@ -459,6 +465,7 @@
|
||||
<string name="automatically_show_url_preview_description">显示 URL 预览</string>
|
||||
<string name="load_image_description">何时加载图像</string>
|
||||
<string name="copy_to_clipboard">复制到剪贴板</string>
|
||||
<string name="copy_npub_to_clipboard">复制 npub 到剪贴板</string>
|
||||
<string name="copy_url_to_clipboard">复制链接到剪贴板</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">复制笔记 ID 到剪贴板</string>
|
||||
<string name="created_at">创建于</string>
|
||||
@@ -522,6 +529,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">无法从闪电地址集合LNURL\"%1$s\"。请检查用户设置</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s 的接收者闪电服务不可用。它是根据雷电地址\"%2$s\"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">无法解析 %1$s。请检查你是否已连接,服务器是否启动,以及闪电地址 %2$s 是否正确</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">无法解析 %1$s。请检查你是否已连接,服务器是否启动,以及闪电地址 %2$s 是否正确。\n\n异常为:%3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">无法从 %1$s 获取发票</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">从闪电地址解析JSON出错。请检查用户的打闪设置</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">在用户闪电地址服务器配置中找不到回调URL</string>
|
||||
@@ -586,4 +594,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">上传后服务器没有提供 URL</string>
|
||||
<string name="could_not_download_from_the_server">无法从服务器下载上传的媒体</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">无法准备要上传的本地文件:%1$s</string>
|
||||
<string name="login_with_qr_code">使用二维码登录</string>
|
||||
<string name="route">路径</string>
|
||||
<string name="route_home">主页</string>
|
||||
<string name="route_search">搜索</string>
|
||||
<string name="route_discover">发现</string>
|
||||
<string name="route_messages">消息</string>
|
||||
<string name="route_notifications">通知</string>
|
||||
<string name="route_global">全球</string>
|
||||
<string name="route_video">短篇</string>
|
||||
<string name="route_security_filters">安全滤镜</string>
|
||||
<string name="new_post">新帖子</string>
|
||||
<string name="new_short">新短篇媒体:图像或视频</string>
|
||||
<string name="new_community_note">新社区笔记</string>
|
||||
<string name="open_all_reactions_to_this_post">展开对此帖子的所有回应</string>
|
||||
<string name="close_all_reactions_to_this_post">收起对此帖子的所有回应</string>
|
||||
<string name="reply_description">回复</string>
|
||||
<string name="boost_or_quote_description">提升或引用</string>
|
||||
<string name="like_description">点赞</string>
|
||||
<string name="zap_description">打闪</string>
|
||||
<string name="profile_image_of_user">%1$s 的个人头像</string>
|
||||
<string name="relay_info">中继器 %1$s</string>
|
||||
<string name="expand_relay_list">展开中继列表</string>
|
||||
<string name="note_options">笔记选项</string>
|
||||
<string name="relay_list_selector">中继列表选择器</string>
|
||||
<string name="poll">投票</string>
|
||||
<string name="disable_poll">停用投票</string>
|
||||
<string name="add_bitcoin_invoice">比特币发票</string>
|
||||
<string name="cancel_bitcoin_invoice">取消比特币发票</string>
|
||||
<string name="cancel_classifieds">取消出售物品</string>
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">取消 Zapraiser</string>
|
||||
<string name="add_location">地点</string>
|
||||
<string name="remove_location">移除地点</string>
|
||||
<string name="add_zap_split">打闪拆分</string>
|
||||
<string name="cancel_zap_split">取消打闪拆分</string>
|
||||
<string name="add_content_warning">添加内容警告</string>
|
||||
<string name="remove_content_warning">移除内容警告</string>
|
||||
<string name="show_npub_as_a_qr_code">将 npub 显示为二维码</string>
|
||||
<string name="invalid_nip19_uri">地址无效</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst 收到了要打开的 URI,但该 uri 无效:%1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -172,12 +172,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<string name="point_to_the_qr_code">對準 QR</string>
|
||||
<string name="show_qr">顯示 QR</string>
|
||||
<string name="profile_image">頭像圖片</string>
|
||||
<string name="your_profile_image">頭像圖片</string>
|
||||
<string name="scan_qr">掃描 QR</string>
|
||||
<string name="show_anyway">一直顯示</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">貼文被標記為不當</string>
|
||||
@@ -90,6 +91,8 @@
|
||||
<string name="add_a_relay">添加中繼器</string>
|
||||
<string name="display_name">顯示名稱</string>
|
||||
<string name="my_display_name">我的顯示名稱</string>
|
||||
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||
<string name="welcome">歡迎!</string>
|
||||
<string name="username">用户名</string>
|
||||
<string name="my_username">我的用户名</string>
|
||||
<string name="about_me">關於我</string>
|
||||
@@ -143,7 +146,14 @@
|
||||
<string name="terms_of_use">使用條款</string>
|
||||
<string name="acceptance_of_terms_is_required">需要接受條款</string>
|
||||
<string name="key_is_required">需要密鑰</string>
|
||||
<string name="name_is_required">名稱為必填</string>
|
||||
<string name="login">登錄</string>
|
||||
<string name="sign_up">註冊</string>
|
||||
<string name="create_account">建立帳戶</string>
|
||||
<string name="how_should_we_call_you">我們應該如何稱呼你?</string>
|
||||
<string name="don_t_have_an_account">還沒有 Nostr 帳戶?</string>
|
||||
<string name="already_have_an_account">已經有 Nostr 帳戶?</string>
|
||||
<string name="create_a_new_account">創建新帳戶</string>
|
||||
<string name="generate_a_new_key">生成新密鑰</string>
|
||||
<string name="loading_feed">正在加載</string>
|
||||
<string name="loading_account">正在加載帳戶</string>
|
||||
@@ -185,12 +195,6 @@
|
||||
<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>
|
||||
@@ -444,6 +448,7 @@
|
||||
<string name="new_feature_nip24_might_not_be_available_description">啟用此模式需要 Amethyst 發送一條 NIP-24 消息(包裝的、密封的私信和群聊消息)。因為 NIP-24 是新的,大多數客戶端尚未執行。請確保接收方正在使用兼容的客戶端。</string>
|
||||
<string name="new_feature_nip24_activate">啟用</string>
|
||||
<string name="messages_create_public_chat">公開</string>
|
||||
<string name="messages_create_public_private_chat_desription">新建公開或私人群組</string>
|
||||
<string name="messages_new_message">私人</string>
|
||||
<string name="messages_new_message_to">至</string>
|
||||
<string name="messages_new_message_subject">主題</string>
|
||||
@@ -460,6 +465,7 @@
|
||||
<string name="automatically_show_url_preview_description">顯示鏈接預覽</string>
|
||||
<string name="load_image_description">何時加載圖像</string>
|
||||
<string name="copy_to_clipboard">複製至剪貼簿</string>
|
||||
<string name="copy_npub_to_clipboard">複製 npub 至剪貼簿</string>
|
||||
<string name="copy_url_to_clipboard">將 URL 複製到剪貼簿</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">將筆記 ID 複製到剪貼簿</string>
|
||||
<string name="created_at">創建於</string>
|
||||
@@ -523,6 +529,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">無法從閃電地址集合 LNURL “%1$s”。請檢查用戶設置</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s 的接收方閃電服務不可用。它是根據閃電地址“%2$s”計算的。錯誤:%3$s。請檢查伺服器是否已上線,閃電地址是否正確</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">無法解析 %1$s。請檢查你是否已連接,伺服器是否啟動,以及閃電地址 %2$s 是否正確</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">無法解析 %1$s。請檢查你是否已連接,服務器是否啓動,以及閃電地址 %2$s 是否正確。\n\n異常爲:%3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">無法從 %1$s 獲取發票</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">從閃電地址解析 JSON 發生錯誤。請檢查用戶的閃電設置</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">在用戶閃電地址伺服器配置中找不到回調 URL</string>
|
||||
@@ -587,4 +594,44 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">上傳後伺服器沒有提供 URL</string>
|
||||
<string name="could_not_download_from_the_server">無法從伺服器下載上傳的媒體</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">無法準備要上傳的本地文件:%1$s</string>
|
||||
<string name="login_with_qr_code">使用二維碼登錄</string>
|
||||
<string name="route">路徑</string>
|
||||
<string name="route_home">主頁</string>
|
||||
<string name="route_search">搜索</string>
|
||||
<string name="route_discover">發現</string>
|
||||
<string name="route_messages">訊息</string>
|
||||
<string name="route_notifications">通知</string>
|
||||
<string name="route_global">全球</string>
|
||||
<string name="route_video">短篇</string>
|
||||
<string name="route_security_filters">安全濾鏡</string>
|
||||
<string name="new_post">新帖子</string>
|
||||
<string name="new_short">新短篇:圖像或視頻</string>
|
||||
<string name="new_community_note">新社區筆記</string>
|
||||
<string name="open_all_reactions_to_this_post">展開所有對此帖子的回應</string>
|
||||
<string name="close_all_reactions_to_this_post">收起所有對此帖子的回應</string>
|
||||
<string name="reply_description">回覆</string>
|
||||
<string name="boost_or_quote_description">提升或引用</string>
|
||||
<string name="like_description">點贊</string>
|
||||
<string name="zap_description">打閃</string>
|
||||
<string name="profile_image_of_user">%1$s 的頭像圖片</string>
|
||||
<string name="relay_info">中繼器 %1$s</string>
|
||||
<string name="expand_relay_list">展開中繼列表</string>
|
||||
<string name="note_options">筆記選項</string>
|
||||
<string name="relay_list_selector">中繼列表選擇器</string>
|
||||
<string name="poll">投票</string>
|
||||
<string name="disable_poll">停用投票</string>
|
||||
<string name="add_bitcoin_invoice">比特幣發票</string>
|
||||
<string name="cancel_bitcoin_invoice">取消比特幣發票</string>
|
||||
<string name="cancel_classifieds">取消出售物品</string>
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">取消 Zapraiser</string>
|
||||
<string name="add_location">地點</string>
|
||||
<string name="remove_location">移除地點</string>
|
||||
<string name="add_zap_split">打閃拆分</string>
|
||||
<string name="cancel_zap_split">取消打閃拆分</string>
|
||||
<string name="add_content_warning">添加內容警告</string>
|
||||
<string name="remove_content_warning">移除內容警告</string>
|
||||
<string name="show_npub_as_a_qr_code">將 npub 顯示爲二維碼</string>
|
||||
<string name="invalid_nip19_uri">地址無效</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst 收到了要打開的 URI,但該 URI 無效:%1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -176,11 +176,6 @@
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
|
||||
<string name="point_to_the_qr_code">Point to the QR Code</string>
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="profile_image">Profile Image</string>
|
||||
<string name="profile_image">Profile Picture</string>
|
||||
<string name="your_profile_image">Your Profile Picture</string>
|
||||
<string name="scan_qr">Scan QR</string>
|
||||
<string name="show_anyway">Show Anyway</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post was muted or reported by</string>
|
||||
@@ -141,12 +142,15 @@
|
||||
<string name="clear">Clear</string>
|
||||
<string name="app_logo">App Logo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec.. or npub..</string>
|
||||
<string name="ncryptsec_password">password to open the key</string>
|
||||
<string name="show_password">Show Password</string>
|
||||
<string name="hide_password">Hide Password</string>
|
||||
<string name="invalid_key">Invalid key</string>
|
||||
<string name="invalid_key_with_message">Invalid key: %1$s</string>
|
||||
<string name="i_accept_the">"I accept the "</string>
|
||||
<string name="terms_of_use">terms of use</string>
|
||||
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
||||
<string name="password_is_required">Password is required</string>
|
||||
<string name="key_is_required">Key is required</string>
|
||||
<string name="name_is_required">A name is required</string>
|
||||
<string name="login">Login</string>
|
||||
@@ -199,15 +203,22 @@
|
||||
<string name="mark_all_new_as_read">Mark all New as read</string>
|
||||
<string name="mark_all_as_read">Mark all as read</string>
|
||||
<string name="backup_keys">Backup Keys</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos">
|
||||
## Key Backup and Safety Tips
|
||||
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
||||
\n\nYour account is secured by a secret key. The key is a long sequence of characters starting with **nsec1**. Anyone who has access to this secret key can post and change your identity.
|
||||
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||
\n- Amethyst developers will **never** ask you for your secret key.
|
||||
\n- Amethyst developers will **never** ask for your secret key.
|
||||
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
||||
</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos">
|
||||
For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password.
|
||||
\n\nIf you lose your password, you will not be able to recover your key.
|
||||
</string>
|
||||
|
||||
<string name="failed_to_encrypt_key">Failed to encrypt your private key</string>
|
||||
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
||||
<string name="copy_my_secret_key">Copy my secret key</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">Encrypt and copy my secret key</string>
|
||||
<string name="biometric_authentication_failed">Authentication failed</string>
|
||||
<string name="biometric_authentication_failed_explainer">Biometrics failed to authenticate the owner of this phone</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">Biometrics failed to authenticate the owner of this phone. Error: %1$s</string>
|
||||
@@ -523,6 +534,7 @@
|
||||
<string name="new_feature_nip24_activate">Activate</string>
|
||||
|
||||
<string name="messages_create_public_chat">Public</string>
|
||||
<string name="messages_create_public_private_chat_desription">New Public or Private Group</string>
|
||||
<string name="messages_new_message">Private</string>
|
||||
<string name="messages_new_message_to">To</string>
|
||||
<string name="messages_new_message_subject">Subject</string>
|
||||
@@ -543,6 +555,7 @@
|
||||
<string name="load_image_description">When to load images</string>
|
||||
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
<string name="copy_npub_to_clipboard">Copy npub to clipboard</string>
|
||||
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
||||
|
||||
@@ -702,4 +715,55 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Server did not provide a URL after uploading</string>
|
||||
<string name="could_not_download_from_the_server">Could not download uploaded media from the server</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string>
|
||||
|
||||
<string name="login_with_qr_code">Login with QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Home</string>
|
||||
<string name="route_search">Search</string>
|
||||
<string name="route_discover">Discover</string>
|
||||
<string name="route_messages">Messages</string>
|
||||
<string name="route_notifications">Notifications</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Shorts</string>
|
||||
<string name="route_security_filters">Security Filters</string>
|
||||
|
||||
<string name="new_post">New Post</string>
|
||||
<string name="new_short">New Shorts: images or videos</string>
|
||||
<string name="new_community_note">New Community Note</string>
|
||||
|
||||
<string name="open_all_reactions_to_this_post">Open all reactions to this post</string>
|
||||
<string name="close_all_reactions_to_this_post">Close all reactions to this post</string>
|
||||
|
||||
<string name="reply_description">Reply</string>
|
||||
<string name="boost_or_quote_description">Boost Or Quote</string>
|
||||
<string name="like_description">Like</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
|
||||
<string name="profile_image_of_user">Profile Picture of %1$s</string>
|
||||
<string name="relay_info">Relay %1$s</string>
|
||||
<string name="expand_relay_list">Expand relay list</string>
|
||||
<string name="note_options">Note options</string>
|
||||
<string name="relay_list_selector">Relay list selector</string>
|
||||
<string name="poll">Poll</string>
|
||||
<string name="disable_poll">Disable Poll</string>
|
||||
|
||||
<string name="add_bitcoin_invoice">Bitcoin Invoice</string>
|
||||
<string name="cancel_bitcoin_invoice">Cancel Bitcoin Invoice</string>
|
||||
<string name="cancel_classifieds">Cancel Sell an Item</string>
|
||||
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">Cancel Zapraiser</string>
|
||||
|
||||
<string name="add_location">Location</string>
|
||||
<string name="remove_location">Remove Location</string>
|
||||
|
||||
<string name="add_zap_split">Zap splits</string>
|
||||
<string name="cancel_zap_split">Cancel Zap split</string>
|
||||
|
||||
<string name="add_content_warning">Add content warning</string>
|
||||
<string name="remove_content_warning">Remove content warning</string>
|
||||
<string name="show_npub_as_a_qr_code">Show npub as a QR code</string>
|
||||
|
||||
<string name="invalid_nip19_uri">Invalid address</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst received a URI to open but that uri was invalid: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -54,6 +54,7 @@ dependencies {
|
||||
androidTestImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3'
|
||||
androidTestImplementation project(path: ':quartz')
|
||||
androidTestImplementation project(path: ':commons')
|
||||
|
||||
// Add your dependencies here. Note that you cannot benchmark code
|
||||
// in an app module this way - you will need to move any code you
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.ExpandableTextParser
|
||||
import com.vitorpamplona.amethyst.commons.nthIndexOf
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExpandableViewComputationBenchmark {
|
||||
@get:Rule
|
||||
val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun computeTestCase1() {
|
||||
benchmarkRule.measureRepeated {
|
||||
TestCase.assertEquals(
|
||||
293,
|
||||
testCase1.nthIndexOf('\n', 10),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeTestCase2() {
|
||||
benchmarkRule.measureRepeated {
|
||||
TestCase.assertEquals(
|
||||
423,
|
||||
testCase2.nthIndexOf('\n', 10),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeTestCase1All() {
|
||||
benchmarkRule.measureRepeated {
|
||||
TestCase.assertEquals(
|
||||
293,
|
||||
ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeTestCase2All() {
|
||||
benchmarkRule.measureRepeated {
|
||||
TestCase.assertEquals(
|
||||
355,
|
||||
ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeTestCase3All() {
|
||||
benchmarkRule.measureRepeated {
|
||||
TestCase.assertEquals(
|
||||
65,
|
||||
ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase3),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val testCase1 = """
|
||||
#Amethyst v0.83.10
|
||||
|
||||
تحديث جديد لـ Amethyst بإصدار 0.83.10 مع تعديلات وإضافات جديدة
|
||||
|
||||
: NIP-92 إصلاحات الأخطاء
|
||||
|
||||
الإضافات الجديدة:
|
||||
- يتضمن رابط المنتج في الرسالة الأولى من المشتري في السوق
|
||||
- يضيف دعمًا لـ NIP-92 في الرسائل العامة والرسائل المباشرة الجديدة (NIP-17). يبقى NIP-54 في NIP-04 DMs
|
||||
- إضافة التمرير الأفقي إلى أزرار الإجراءات في شاشة النشر الجديد لإصلاح الأزرار المخفية جزئيًا في الشاشات الصغيرة/الرفيعة.
|
||||
|
||||
اصلاحات الشوائب:
|
||||
- إصلاحات التعطل مع مبلغ Zap مخصص غير صالح
|
||||
- يعمل على إصلاح مشكلات إعادة اتصال التتابع عندما يقوم المرحل بإغلاق الاتصال
|
||||
- إصلاح الحشو العلوي للملاحظة المقتبسة في المنشور
|
||||
- تحسين استخدام الذاكرة للمستخدم المرئي وعلامة URL في المشاركات الجديدة
|
||||
|
||||
الترجمات المحدثة:
|
||||
- الفارسية بواسطة
|
||||
- الفرنسية والإنجليزية، المملكة المتحدة بواسطة
|
||||
- الأوكرانية
|
||||
- الإسبانية والإسبانية والمكسيك والإسبانية والولايات المتحدة بواسطة
|
||||
- العربية
|
||||
|
||||
تحسينات جودة الكود:
|
||||
- تحديثات لنظام Android Studio 2023.1.1 Patch 2
|
||||
|
||||
|
||||
|
||||
|
||||
nostr:nevent1qqszq7kl888sw0c5rpvepn8w373zt0jrw8864x8lkauxxw335s66rzgppemhxue69uhkummn9ekx7mp0qgsyvrp9u6p0mfur9dfdru3d853tx9mdjuhkphxuxgfwmryja7zsvhqrqsqqqqqpaax7m2
|
||||
"""
|
||||
|
||||
val testCase2 = """
|
||||
#Amethyst v0.83.10: NIP-92 and Bug Fixes
|
||||
|
||||
New Additions:
|
||||
- Includes a link to the product in the first message from the buyer in the marketplace
|
||||
- Adds support for NIP-92 in public messages and new DMs (NIP-17). NIP-54 stays in NIP-04 DMs
|
||||
- Adds Horizontal Scroll to the action buttons in the New Post screen to partially fix hidden buttons in small/thin screens.
|
||||
|
||||
Bugfixes:
|
||||
- Fixes crash with an invalid custom Zap Amount
|
||||
- Fixes relay re-connection issues when the relay closes a connection
|
||||
- Fixes the top padding of the quoted note in a post
|
||||
- Optimizes memory use of the visual user and url tagger in new posts
|
||||
|
||||
Updated translations:
|
||||
- Persian by nostr:npub1cpazafytvafazxkjn43zjfwtfzatfz508r54f6z6a3rf2ws8223qc3xxpk
|
||||
- French and English, United Kingdom by nostr:npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
|
||||
- Ukrainian by crowdin.com/profile/liizzzz
|
||||
- Spanish, Spanish, Mexico and Spanish, United States by nostr:npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
|
||||
- Arabic by nostr:npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
|
||||
|
||||
Code Quality Improvements:
|
||||
- Updates to Android Studio 2023.1.1 Patch 2
|
||||
|
||||
Download:
|
||||
- [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk )
|
||||
- [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-fdroid-universal-v0.83.10.apk )
|
||||
"""
|
||||
|
||||
val testCase3 = """#100aDayUntil100k
|
||||
Day 5 ✔️
|
||||
|
||||
Seems like they may be getting easier"""
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
+3
-3
@@ -4,15 +4,15 @@ buildscript {
|
||||
ext {
|
||||
fragment_version = "1.6.2"
|
||||
lifecycle_version = '2.7.0'
|
||||
compose_ui_version = '1.6.0'
|
||||
nav_version = '2.7.6'
|
||||
compose_ui_version = '1.6.1'
|
||||
nav_version = '2.7.7'
|
||||
room_version = "2.4.3"
|
||||
accompanist_version = '0.34.0'
|
||||
coil_version = '2.5.0'
|
||||
vico_version = '1.13.1'
|
||||
media3_version = '1.2.1'
|
||||
core_ktx_version = '1.12.0'
|
||||
material3_version = '1.1.2'
|
||||
material3_version = '1.2.0'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.vitorpamplona.amethyst.commons'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk 26
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
freeCompilerArgs += "-Xstring-concat=inline"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons
|
||||
|
||||
class ExpandableTextParser {
|
||||
companion object {
|
||||
private const val SHORT_TEXT_LENGTH = 350
|
||||
private const val SHORTEN_AFTER_LINES = 10
|
||||
|
||||
fun computeWhereToCutIfPostIsTooLong(content: String): Int {
|
||||
// Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters
|
||||
val firstSpaceAfterCut =
|
||||
content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
val firstNewLineAfterCut =
|
||||
content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
val firstLineAfterLineLimits =
|
||||
content.nthIndexOf('\n', SHORTEN_AFTER_LINES).let { if (it < 0) content.length else it }
|
||||
|
||||
return minOf(firstSpaceAfterCut, firstNewLineAfterCut, firstLineAfterLineLimits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.nthIndexOf(
|
||||
ch: Char,
|
||||
N: Int,
|
||||
): Int {
|
||||
var occur = N
|
||||
var pos = -1
|
||||
|
||||
while (occur > 0) {
|
||||
// calling the native function multiple times is faster than looping just once
|
||||
pos = indexOf(ch, pos + 1)
|
||||
if (pos == -1) return -1
|
||||
occur--
|
||||
}
|
||||
|
||||
return if (occur == 0) pos else -1
|
||||
}
|
||||
@@ -34,6 +34,10 @@ android {
|
||||
jvmTarget = '17'
|
||||
freeCompilerArgs += "-Xstring-concat=inline"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '**/libscrypt.dylib'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -54,6 +58,8 @@ dependencies {
|
||||
// immutable collections to avoid recomposition
|
||||
api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7')
|
||||
|
||||
api('com.lambdaworks:scrypt:1.4.0')
|
||||
|
||||
// Parses URLs from Text:
|
||||
api "io.github.url-detector:url-detector:0.1.23"
|
||||
|
||||
|
||||
Vendored
+5
@@ -33,6 +33,11 @@
|
||||
# JNA For Libsodium
|
||||
-keep class com.goterl.lazysodium.** { *; }
|
||||
|
||||
# libscrypt
|
||||
-keep class com.lambdaworks.codec.** { *; }
|
||||
-keep class com.lambdaworks.crypto.** { *; }
|
||||
-keep class com.lambdaworks.jni.** { *; }
|
||||
|
||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
||||
-keep class com.sun.jna.NativeMapped { *; }
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.crypto.Nip49
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.security.SecureRandom
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
public class NIP49Test {
|
||||
companion object {
|
||||
val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p"
|
||||
|
||||
val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"
|
||||
val TEST_CASE_PASSWORD = "nostr"
|
||||
|
||||
val MAIN_TEST_CASES =
|
||||
listOf<Nip49TestCase>(
|
||||
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00),
|
||||
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01),
|
||||
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02),
|
||||
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00),
|
||||
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01),
|
||||
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02),
|
||||
)
|
||||
}
|
||||
|
||||
val random = SecureRandom()
|
||||
val nip49 = Nip49(Secp256k1.get(), random)
|
||||
|
||||
@Test
|
||||
fun decodeBech32() {
|
||||
val data =
|
||||
Nip49.EncryptedInfo.decodePayload(
|
||||
TEST_CASE,
|
||||
)!!
|
||||
|
||||
assertEquals(2, data.version)
|
||||
assertEquals(16, data.logn)
|
||||
assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey())
|
||||
assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey())
|
||||
assertEquals(0, data.keySecurity)
|
||||
assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decrypt() {
|
||||
val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD)
|
||||
assertEquals(TEST_CASE_EXPECTED, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptDecryptTestCase() {
|
||||
val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0)
|
||||
val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD)
|
||||
|
||||
assertEquals(TEST_CASE_EXPECTED, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptDecrypt() {
|
||||
MAIN_TEST_CASES.forEach {
|
||||
val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb)
|
||||
|
||||
assertNotNull(encrypted)
|
||||
|
||||
val decrypted = nip49.decrypt(encrypted!!, it.password)
|
||||
|
||||
assertEquals(it.secretKey, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
class Nip49TestCase(
|
||||
val password: String,
|
||||
val secretKey: String,
|
||||
val logn: Int,
|
||||
val ksb: Byte,
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.crypto
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
import java.security.MessageDigest
|
||||
@@ -34,6 +35,7 @@ object CryptoUtils {
|
||||
private val nip04 = Nip04(secp256k1, random)
|
||||
private val nip44v1 = Nip44v1(secp256k1, random)
|
||||
private val nip44v2 = Nip44v2(secp256k1, random)
|
||||
private val nip49 = Nip49(secp256k1, random)
|
||||
|
||||
fun clearCache() {
|
||||
nip04.clearCache()
|
||||
@@ -262,6 +264,21 @@ object CryptoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptNIP49(
|
||||
payload: String,
|
||||
password: String,
|
||||
): String? {
|
||||
if (payload.isEmpty() || password.isEmpty()) return null
|
||||
return nip49.decrypt(payload, password)
|
||||
}
|
||||
|
||||
fun encryptNIP49(
|
||||
key: HexKey,
|
||||
password: String,
|
||||
): String? {
|
||||
return nip49.encrypt(key, password, 16, Nip49.EncryptedInfo.CLIENT_DOES_NOT_TRACK)
|
||||
}
|
||||
|
||||
class EncryptedInfoString(
|
||||
val ciphertext: String,
|
||||
val nonce: String,
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.crypto
|
||||
|
||||
import android.util.Log
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.lambdaworks.crypto.SCrypt
|
||||
import com.vitorpamplona.quartz.encoders.Bech32
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
import java.security.SecureRandom
|
||||
|
||||
class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
|
||||
private val libSodium = SodiumAndroid()
|
||||
private val lazySodium = LazySodiumAndroid(libSodium)
|
||||
|
||||
fun decrypt(
|
||||
nCryptSec: String,
|
||||
password: String,
|
||||
): HexKey {
|
||||
return decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
encryptedInfo: EncryptedInfo?,
|
||||
password: String = "",
|
||||
): String {
|
||||
check(encryptedInfo != null) { "Couldn't decode key" }
|
||||
check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" }
|
||||
|
||||
val n = Math.pow(2.0, encryptedInfo.logn.toDouble()).toInt()
|
||||
val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), encryptedInfo.salt, n, 8, 1, 32)
|
||||
val m = ByteArray(32)
|
||||
|
||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
m,
|
||||
longArrayOf(32L),
|
||||
key,
|
||||
encryptedInfo.encryptedKey,
|
||||
encryptedInfo.encryptedKey.size.toLong(),
|
||||
byteArrayOf(encryptedInfo.keySecurity),
|
||||
1,
|
||||
encryptedInfo.nonce,
|
||||
key,
|
||||
)
|
||||
|
||||
check(m.any { it > 0 }) { "Incorrect password" }
|
||||
|
||||
return m.toHexKey()
|
||||
}
|
||||
|
||||
fun encrypt(
|
||||
secretKeyHex: String,
|
||||
password: String,
|
||||
logn: Int,
|
||||
ksb: Byte,
|
||||
): String? {
|
||||
return encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
|
||||
}
|
||||
|
||||
fun encrypt(
|
||||
secretKey: ByteArray,
|
||||
password: String,
|
||||
logn: Int,
|
||||
ksb: Byte,
|
||||
): String {
|
||||
check(secretKey.size == 32) { "invalid secret key" }
|
||||
val salt = ByteArray(16)
|
||||
random.nextBytes(salt)
|
||||
|
||||
val nonce = ByteArray(24)
|
||||
random.nextBytes(nonce)
|
||||
|
||||
val n = Math.pow(2.0, logn.toDouble()).toInt()
|
||||
val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), salt, n, 8, 1, 32)
|
||||
val ciphertext = ByteArray(48)
|
||||
|
||||
// byte[] c, long[] cLen,
|
||||
// byte[] m, long mLen,
|
||||
// byte[] ad, long adLen,
|
||||
// byte[] nSec, byte[] nPub, byte[] k
|
||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
||||
ciphertext, longArrayOf(48),
|
||||
secretKey, secretKey.size.toLong(),
|
||||
byteArrayOf(ksb), 1,
|
||||
key,
|
||||
nonce,
|
||||
key,
|
||||
)
|
||||
|
||||
return EncryptedInfo(
|
||||
EncryptedInfo.V,
|
||||
logn.toByte(),
|
||||
salt,
|
||||
nonce,
|
||||
ksb,
|
||||
ciphertext,
|
||||
).encodePayload()
|
||||
}
|
||||
|
||||
class EncryptedInfo(
|
||||
val version: Byte,
|
||||
val logn: Byte,
|
||||
val salt: ByteArray,
|
||||
val nonce: ByteArray,
|
||||
val keySecurity: Byte,
|
||||
val encryptedKey: ByteArray,
|
||||
) {
|
||||
companion object {
|
||||
const val V: Byte = 0x02
|
||||
|
||||
const val UNSAFE_HANDLING: Byte = 0x00
|
||||
const val SAFE_HANDLING: Byte = 0x01
|
||||
const val CLIENT_DOES_NOT_TRACK: Byte = 0x02
|
||||
|
||||
fun decodePayload(nCryptSec: String): EncryptedInfo? {
|
||||
val byteArray =
|
||||
try {
|
||||
nCryptSec.bechToBytes()
|
||||
} catch (e: Throwable) {
|
||||
Log.e("NIP19 Parser", "Issue trying to Decode NIP49 $nCryptSec: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
return EncryptedInfo(
|
||||
version = byteArray[0],
|
||||
logn = byteArray[1],
|
||||
salt = byteArray.copyOfRange(2, 2 + 16),
|
||||
nonce = byteArray.copyOfRange(2 + 16, 2 + 16 + 24),
|
||||
keySecurity = byteArray.copyOfRange(2 + 16 + 24, 2 + 16 + 24 + 1).get(0),
|
||||
encryptedKey = byteArray.copyOfRange(2 + 16 + 24 + 1, byteArray.size),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP44v2", "Unable to Parse encrypted ncryptsec payload")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ln(n.toDouble()).toInt().toByte(),
|
||||
fun encodePayload(): String {
|
||||
return Bech32.encodeBytes(
|
||||
hrp = "ncryptsec",
|
||||
byteArrayOf(
|
||||
version,
|
||||
logn,
|
||||
) + salt + nonce + keySecurity + encryptedKey,
|
||||
Bech32.Encoding.Bech32,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user