diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index c1346cc53..9e2770411 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -112,6 +112,28 @@ jobs: asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk asset_content_type: application/zip + - name: Upload Play APK arm64-v8a Asset + id: upload-release-asset-play-arm64-v8a-apk + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk + asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk + asset_content_type: application/zip + + - name: Upload Play APK armeabi-v7a Asset + id: upload-release-asset-play-armeabi-v7a-apk + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk + asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk + asset_content_type: application/zip + # F-Droid APK - name: Upload F-Droid APK Universal Asset id: upload-release-asset-fdroid-universal-apk @@ -146,6 +168,28 @@ jobs: asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk asset_content_type: application/zip + - name: Upload F-Droid APK arm64-v8a Asset + id: upload-release-asset-fdroid-arm64-v8a-apk + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk + asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk + asset_content_type: application/zip + + - name: Upload F-Droid APK armeabi-v7a Asset + id: upload-release-asset-fdroid-armeabi-v7a-apk + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk + asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk + asset_content_type: application/zip + # Google Play AAB diff --git a/app/build.gradle b/app/build.gradle index 8fc23db2b..219aa8c2c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ android { vectorDrawables { useSupportLibrary true } - resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW'] + resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'in', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW'] } buildTypes { @@ -54,7 +54,7 @@ android { abi { enable true reset() - include "x86", "x86_64" + include "x86", "x86_64", "arm64-v8a", "armeabi-v7a" universalApk true } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 12661f185..0043d4054 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2628,14 +2628,18 @@ class Account( // Ugly, but forces nostr.band as the only search-supporting relay today. // TODO: Remove when search becomes more available. - if (usersRelayList.none { it.activeTypes.contains(FeedType.SEARCH) }) { - usersRelayList = usersRelayList + Relay( - Constants.forcedRelayForSearch.url, - Constants.forcedRelayForSearch.read, - Constants.forcedRelayForSearch.write, - Constants.forcedRelayForSearch.feedTypes, - proxy - ) + val searchRelays = usersRelayList.filter { it.url.removeSuffix("/") in Constants.forcedRelaysForSearchSet } + val hasSearchRelay = usersRelayList.any { it.activeTypes.contains(FeedType.SEARCH) } + if (!hasSearchRelay && searchRelays.isEmpty()) { + usersRelayList = usersRelayList + Constants.forcedRelayForSearch.map { + Relay( + it.url, + it.read, + it.write, + it.feedTypes, + proxy + ) + } } return usersRelayList.toTypedArray() @@ -2653,6 +2657,10 @@ class Account( .toTypedArray() } + fun activeWriteRelays(): List { + return (activeRelays() ?: convertLocalRelays()).filter { it.write } + } + fun reconnectIfRelaysHaveChanged() { val newRelaySet = activeRelays() ?: convertLocalRelays() if (!Client.isSameRelaySetConfig(newRelaySet)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 63788310a..35ebae885 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -734,6 +734,12 @@ class NoteLiveSet(u: Note) { it.note.boosts.toImmutableList() }.distinctUntilChanged() + val relayInfo = innerRelays.map { + it.note.relays.map { + RelayBriefInfo(it) + }.toImmutableList() + } + fun isInUse(): Boolean { return metadata.hasObservers() || reactions.hasObservers() || @@ -812,3 +818,10 @@ class NoteLoadingLiveData(val note: Note, initialValue: Y?) : MediatorLiveDat @Immutable class NoteState(val note: Note) + +@Immutable +data class RelayBriefInfo( + val url: String, + val displayUrl: String = url.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/").intern(), + val favIcon: String = "https://$displayUrl/favicon.ico".intern() +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt index c9f099ed6..18782e29f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt @@ -14,4 +14,6 @@ data class RelaySetupInfo( val spamCount: Int = 0, val feedTypes: Set, val paidRelay: Boolean = false -) +) { + val briefInfo: RelayBriefInfo = RelayBriefInfo(url) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt new file mode 100644 index 000000000..58e88e207 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11Retriever.kt @@ -0,0 +1,103 @@ +package com.vitorpamplona.amethyst.service + +import android.util.Log +import android.util.LruCache +import com.vitorpamplona.amethyst.model.RelayInformation +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +object Nip11CachedRetriever { + val relayInformationDocumentCache = LruCache(100) + val retriever = Nip11Retriever() + + suspend fun loadRelayInfo( + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit + ) { + val url = retriever.cleanUrl(dirtyUrl) + val doc = relayInformationDocumentCache.get(url) + + if (doc != null) { + onInfo(doc) + } else { + Nip11Retriever().loadRelayInfo( + url, + dirtyUrl, + onInfo = { + relayInformationDocumentCache.put(url, it) + onInfo(it) + }, + onError + ) + } + } +} + +class Nip11Retriever { + enum class ErrorCode { + FAIL_TO_ASSEMBLE_URL, + FAIL_TO_REACH_SERVER, + FAIL_TO_PARSE_RESULT, + FAIL_WITH_HTTP_STATUS + } + + fun cleanUrl(dirtyUrl: String): String { + return if (dirtyUrl.contains("://")) { + dirtyUrl + .replace("wss://", "https://") + .replace("ws://", "http://") + } else { + "https://$dirtyUrl" + } + } + + suspend fun loadRelayInfo( + url: String, + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, ErrorCode, String?) -> Unit + ) { + try { + val request: Request = Request + .Builder() + .header("Accept", "application/nostr+json") + .url(url) + .build() + + HttpClient.getHttpClient() + .newCall(request) + .enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + checkNotInMainThread() + response.use { + val body = it.body.string() + try { + if (it.isSuccessful) { + onInfo(RelayInformation.fromJson(body)) + } else { + onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) + } + } catch (e: Exception) { + Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) + } + } + } + + override fun onFailure(call: Call, e: IOException) { + Log.e("RelayInfoFail", "$dirtyUrl unavailable", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message) + } + } + ) + } catch (e: Exception) { + Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) + onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 55f01d3b4..373b8aa65 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean import kotlin.Error abstract class NostrDataSource(val debugName: String) { @@ -23,6 +24,7 @@ abstract class NostrDataSource(val debugName: String) { data class Counter(var counter: Int) private var eventCounter = mapOf() + var changingFilters = AtomicBoolean() fun printCounter() { eventCounter.forEach { @@ -59,11 +61,17 @@ abstract class NostrDataSource(val debugName: String) { if (type == Relay.Type.EOSE && subscriptionId != null && subscriptionId in subscriptions.keys) { // updates a per subscripton since date - subscriptions[subscriptionId]?.updateEOSE(TimeUtils.now(), relay.url) + subscriptions[subscriptionId]?.updateEOSE( + TimeUtils.fiveMinutesAgo(), // in case people's clock is slighly off. + relay.url + ) } } override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) { + if (success) { + markAsSeenOnRelay(eventId, relay) + } } override fun onAuth(relay: Relay, challenge: String) { @@ -136,6 +144,8 @@ abstract class NostrDataSource(val debugName: String) { // saves the current content to only update if it changes val currentFilters = activeSubscriptions.associate { it.id to it.toJson() } + changingFilters.getAndSet(true) + updateChannelFilters() // Makes sure to only send an updated filter when it actually changes. @@ -167,12 +177,18 @@ abstract class NostrDataSource(val debugName: String) { } } } + + changingFilters.getAndSet(false) } open fun consume(event: Event, relay: Relay) { LocalCache.verifyAndConsume(event, relay) } + open fun markAsSeenOnRelay(eventId: String, relay: Relay) { + LocalCache.getNoteIfExists(eventId)?.addRelay(relay) + } + abstract fun updateChannelFilters() open fun auth(relay: Relay, challenge: String) = Unit } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index deb51116f..91ba45d86 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -134,6 +134,9 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { } val singleEventChannel = requestNewChannel { time, relayUrl -> + // Ignores EOSE if it is in the middle of a filter change. + if (changingFilters.get()) return@requestNewChannel + checkNotInMainThread() eventsToWatch.forEach { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index d62179f8d..186bc3db5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -16,9 +16,7 @@ object Constants { } val defaultRelays = arrayOf( - // Free relays for DMs and Follows - RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes), - RelaySetupInfo("wss://relay.snort.social", read = true, write = true, feedTypes = activeTypes), + // Free relays for only DMs and Follows due to the amount of spam RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes), // Chats @@ -40,6 +38,7 @@ object Constants { // NewRelayListViewModel.Relay("wss://brb.io", read = true, write = true, feedTypes = activeTypes), // Paid relays + RelaySetupInfo("wss://relay.snort.social", read = true, write = false, feedTypes = activeTypesGlobalChats), RelaySetupInfo("wss://relay.nostr.com.au", read = true, write = false, feedTypes = activeTypesGlobalChats), RelaySetupInfo("wss://eden.nostr.land", read = true, write = false, feedTypes = activeTypesGlobalChats), RelaySetupInfo("wss://nostr.milou.lol", read = true, write = false, feedTypes = activeTypesGlobalChats), @@ -51,8 +50,15 @@ object Constants { RelaySetupInfo("wss://relay.nostrati.com", read = true, write = false, feedTypes = activeTypesGlobalChats), // Supporting NIP-50 - RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch) + RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo("wss://filter.nostr.wine", read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch) ) - val forcedRelayForSearch = RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch) + val forcedRelayForSearch = arrayOf( + RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo("wss://filter.nostr.wine", read = true, write = false, feedTypes = activeTypesSearch), + RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch) + ) + val forcedRelaysForSearchSet = forcedRelayForSearch.map { it.url } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 3233307e9..7638d7b45 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -1,9 +1,13 @@ package com.vitorpamplona.amethyst.service.relays -import androidx.lifecycle.LiveData +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow /** * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. @@ -12,6 +16,11 @@ object RelayPool : Relay.Listener { private var relays = listOf() private var listeners = setOf() + // Backing property to avoid flow emissions from other classes + private var _lastStatus = RelayPoolStatus(0, 0) + private val _statusFlow = MutableSharedFlow(1, 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val statusFlow: SharedFlow = _statusFlow.asSharedFlow() + fun availableRelays(): Int { return relays.size } @@ -76,11 +85,13 @@ object RelayPool : Relay.Listener { fun addRelay(relay: Relay) { relay.register(this) relays += relay + updateStatus() } fun removeRelay(relay: Relay) { relay.unregister(this) relays = relays.minus(relay) + updateStatus() } fun register(listener: Listener) { @@ -109,12 +120,14 @@ object RelayPool : Relay.Listener { override fun onError(relay: Relay, subscriptionId: String, error: Error) { listeners.forEach { it.onError(error, subscriptionId, relay) } - refreshObservers() + updateStatus() } override fun onRelayStateChange(relay: Relay, type: Relay.Type, channel: String?) { listeners.forEach { it.onRelayStateChange(type, relay, channel) } - refreshObservers() + if (type != Relay.Type.EOSE) { + updateStatus() + } } override fun onSendResponse(relay: Relay, eventId: String, success: Boolean, message: String) { @@ -125,18 +138,15 @@ object RelayPool : Relay.Listener { listeners.forEach { it.onAuth(relay, challenge) } } - // Observers line up here. - val live: RelayPoolLiveData = RelayPoolLiveData(this) - - private fun refreshObservers() { - live.refresh() + private fun updateStatus() { + val connected = connectedRelays() + val available = availableRelays() + if (_lastStatus.connected != connected || _lastStatus.available != available) { + _lastStatus = RelayPoolStatus(connected, available) + _statusFlow.tryEmit(_lastStatus) + } } } -class RelayPoolLiveData(val relays: RelayPool) : LiveData(RelayPoolState(relays)) { - fun refresh() { - postValue(RelayPoolState(relays)) - } -} - -class RelayPoolState(val relays: RelayPool) +@Immutable +data class RelayPoolStatus(val connected: Int, val available: Int, val isConnected: Boolean = connected > 0) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 396b54f92..4fd53823c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.net.ConnectivityManager @@ -68,7 +69,10 @@ class MainActivity : AppCompatActivity() { themeViewModel.onChange(LocalPreferences.getTheme()) AmethystTheme(themeViewModel) { // A surface container using the 'background' color from the theme - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { val accountStateViewModel: AccountStateViewModel = viewModel { AccountStateViewModel(this@MainActivity) } @@ -84,7 +88,8 @@ class MainActivity : AppCompatActivity() { .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build() - val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager + val connectivityManager = + getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.requestNetwork(networkRequest, networkCallback) } @@ -162,7 +167,8 @@ class MainActivity : AppCompatActivity() { super.onCapabilitiesChanged(network, networkCapabilities) GlobalScope.launch(Dispatchers.IO) { - val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + val hasMobileData = + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData") Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi") @@ -189,9 +195,15 @@ class MainActivity : AppCompatActivity() { class GetMediaActivityResultContract : ActivityResultContracts.GetContent() { + @SuppressLint("MissingSuperCall") override fun createIntent(context: Context, input: String): Intent { - return super.createIntent(context, input).apply { + // Force only images and videos to be selectable + // Force OPEN Document because of the resulting URI must be passed to the + // Playback service and the picker's permissions only allow the activity to read the URI + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) // Force only images and videos to be selectable + type = "*/*" putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*")) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index b4344132c..ab3a29fc6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions import android.graphics.Bitmap import android.net.Uri import android.os.Build +import android.util.Log import android.util.Size import android.widget.Toast import androidx.compose.foundation.Image @@ -79,13 +80,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = account.activeRelays()?.filter { - it.write - }?.map { - it - } ?: account.convertLocalRelays().filter { - it.write - } + var relayList = account.activeWriteRelays() Dialog( onDismissRequest = { onClose() }, @@ -101,7 +96,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac ) { if (showRelaysDialog) { RelaySelectionDialog( - list = relayList, + preSelectedList = relayList, onClose = { showRelaysDialog = false }, @@ -221,7 +216,8 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo try { bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null) } catch (e: Exception) { - postViewModel.imageUploadingError.emit("Unable to load file") + postViewModel.imageUploadingError.emit("Unable to load thumbnail, but the video can be uploaded") + Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index fc43a4293..daf7d30dd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -133,13 +133,7 @@ fun NewPostView( var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = account.activeRelays()?.filter { - it.write - }?.map { - it - } ?: account.convertLocalRelays().filter { - it.write - } + var relayList = account.activeWriteRelays() LaunchedEffect(Unit) { postViewModel.load(account, baseReplyTo, quote) @@ -169,7 +163,7 @@ fun NewPostView( ) { if (showRelaysDialog) { RelaySelectionDialog( - list = relayList, + preSelectedList = relayList, onClose = { showRelaysDialog = false }, @@ -1376,8 +1370,8 @@ fun ImageVideoDescription( try { bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null) } catch (e: Exception) { - onError("Unable to load file") - Log.e("NewPostView", "Couldn't create thumbnail for $uri") + onError("Unable to load thumbnail") + Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 3abfba9ef..1f58535f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -50,24 +49,35 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.service.Nip11Retriever +import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat +import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.WarningColor +import com.vitorpamplona.amethyst.ui.theme.allGoodColor import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.CoroutineScope +import com.vitorpamplona.amethyst.ui.theme.warningColor import kotlinx.coroutines.launch import java.lang.Math.round @@ -75,16 +85,9 @@ import java.lang.Math.round fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) { val postViewModel: NewRelayListViewModel = viewModel() val feedState by postViewModel.relays.collectAsState() - val context = LocalContext.current - val scope = rememberCoroutineScope() LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) - postViewModel.relays.value.forEach { item -> - loadRelayInfo(item.url, context, scope) { - postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) - } - } } Dialog( @@ -96,7 +99,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re TopAppBar( title = { Row( - modifier = Modifier.fillMaxWidth().padding(end = 10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(end = 10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -108,11 +113,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re defaultRelays.forEach { postViewModel.addRelay(it) } - postViewModel.relays.value.forEach { item -> - loadRelayInfo(item.url, context, scope) { - postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) - } - } + postViewModel.loadRelayDocuments() } ) { Text(stringResource(R.string.default_relays)) @@ -139,8 +140,6 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re ) } ) { pad -> - val scope = rememberCoroutineScope() - Column( modifier = Modifier.padding( 16.dp, @@ -158,10 +157,6 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re ) ) { itemsIndexed(feedState, key = { _, item -> item.url }) { index, item -> - if (index == 0) { - ServerConfigHeader() - } - ServerConfig( item, onToggleDownload = { postViewModel.toggleDownload(it) }, @@ -175,9 +170,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re onDelete = { postViewModel.deleteRelay(it) }, accountViewModel = accountViewModel, - nav = nav, - scope = scope, - context = context + nav = nav ) } } @@ -261,13 +254,38 @@ fun ServerConfigHeader() { } } -@OptIn(ExperimentalFoundationApi::class) +@Preview +@Composable +fun ServerConfigPreview() { + ServerConfigClickableLine( + item = RelaySetupInfo( + url = "nostr.mom", + read = true, + write = true, + errorCount = 23, + downloadCountInBytes = 10000, + uploadCountInBytes = 10000000, + spamCount = 10, + feedTypes = Constants.activeTypesGlobalChats, + paidRelay = true + ), + onDelete = {}, + onToggleDownload = {}, + onToggleUpload = {}, + onToggleFollows = {}, + onTogglePrivateDMs = {}, + onTogglePublicChats = {}, + onToggleGlobal = {}, + onToggleSearch = {}, + onClick = {} + ) +} + @Composable fun ServerConfig( item: RelaySetupInfo, onToggleDownload: (RelaySetupInfo) -> Unit, onToggleUpload: (RelaySetupInfo) -> Unit, - onToggleFollows: (RelaySetupInfo) -> Unit, onTogglePrivateDMs: (RelaySetupInfo) -> Unit, onTogglePublicChats: (RelaySetupInfo) -> Unit, @@ -276,393 +294,112 @@ fun ServerConfig( onDelete: (RelaySetupInfo) -> Unit, accountViewModel: AccountViewModel, - nav: (String) -> Unit, - context: Context, - scope: CoroutineScope + nav: (String) -> Unit ) { - var relayInfo: RelayInformation? by remember { mutableStateOf(null) } + var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + val context = LocalContext.current - if (relayInfo != null) { + relayInfo?.let { RelayInformationDialog( - onClose = { - relayInfo = null - }, - relayInfo = relayInfo!!, - accountViewModel, - nav + onClose = { relayInfo = null }, + relayInfo = it.relayInfo, + relayBriefInfo = it.relayBriefInfo, + accountViewModel = accountViewModel, + nav = nav ) } + ServerConfigClickableLine( + item = item, + onToggleDownload = onToggleDownload, + onToggleUpload = onToggleUpload, + onToggleFollows = onToggleFollows, + onTogglePrivateDMs = onTogglePrivateDMs, + onTogglePublicChats = onTogglePublicChats, + onToggleGlobal = onToggleGlobal, + onToggleSearch = onToggleSearch, + onDelete = onDelete, + onClick = { + accountViewModel.retrieveRelayDocument( + item.url, + onInfo = { + relayInfo = RelayInfoDialog(RelayBriefInfo(item.url), it) + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) + } + ) +} + +@Composable +fun ServerConfigClickableLine( + item: RelaySetupInfo, + onToggleDownload: (RelaySetupInfo) -> Unit, + onToggleUpload: (RelaySetupInfo) -> Unit, + onToggleFollows: (RelaySetupInfo) -> Unit, + onTogglePrivateDMs: (RelaySetupInfo) -> Unit, + onTogglePublicChats: (RelaySetupInfo) -> Unit, + onToggleGlobal: (RelaySetupInfo) -> Unit, + onToggleSearch: (RelaySetupInfo) -> Unit, + onDelete: (RelaySetupInfo) -> Unit, + onClick: () -> Unit +) { Column(Modifier.fillMaxWidth()) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp) ) { - Column() { - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onDelete(item) } - ) { - Icon( - imageVector = Icons.Default.Cancel, - null, - modifier = Modifier - .padding(end = 5.dp) - .size(15.dp), - tint = Color.Red - ) - } + Column(Modifier.clickable(onClick = onClick)) { + RenderRelayIcon(iconUrl = item.briefInfo.favIcon, Size55dp) } - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (item.paidRelay) { - Icon( - imageVector = Icons.Default.Paid, - null, - modifier = Modifier - .padding(end = 5.dp) - .size(14.dp), - tint = Color.Green - ) - } + Spacer(modifier = HalfHorzPadding) - Text( - text = item.url.removePrefix("wss://").removeSuffix("/"), - modifier = Modifier - .weight(1f) - .clickable { - loadRelayInfo(item.url, context, scope) { - relayInfo = it - } - }, - maxLines = 1, - overflow = TextOverflow.Ellipsis + Column(Modifier.weight(1f)) { + FirstLine(item, onClick, onDelete, ReactionRowHeightChat.fillMaxWidth()) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat.fillMaxWidth() + ) { + RenderActiveToggles( + item = item, + onToggleFollows = onToggleFollows, + onTogglePrivateDMs = onTogglePrivateDMs, + onTogglePublicChats = onTogglePublicChats, + onToggleGlobal = onToggleGlobal, + onToggleSearch = onToggleSearch ) } - Row(verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - modifier = Modifier - .size(30.dp), - onClick = { } - ) { - Icon( - painterResource(R.drawable.ic_home), - stringResource(R.string.home_feed), - modifier = Modifier - .padding(end = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleFollows(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.home_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - painterResource(R.drawable.ic_dm), - stringResource(R.string.private_message_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePrivateDMs(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.private_message_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Groups, - stringResource(R.string.public_chat_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePublicChats(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.public_chat_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Public, - stringResource(R.string.global_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleGlobal(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.global_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.GLOBAL)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onToggleSearch(item) } - ) { - Icon( - imageVector = Icons.Default.Search, - stringResource(R.string.search_feed), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleSearch(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.search_feed), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.feedTypes.contains(FeedType.SEARCH)) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - } - } - - Column(Modifier.weight(1.4f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Download, - stringResource(R.string.read_from_relay), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleDownload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.read_from_relay), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.read) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - Text( - text = "${countToHumanReadable(item.downloadCountInBytes)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colors.placeholderText - ) - - IconButton( - modifier = Modifier.size(30.dp), - onClick = { } - ) { - Icon( - imageVector = Icons.Default.Upload, - stringResource(R.string.write_to_relay), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleUpload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.write_to_relay), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.write) { - Color.Green - } else { - MaterialTheme.colors.onSurface.copy( - alpha = 0.32f - ) - } - ) - } - - Text( - text = "${countToHumanReadable(item.uploadCountInBytes)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1.2f), - color = MaterialTheme.colors.placeholderText - ) - - Icon( - imageVector = Icons.Default.SyncProblem, - stringResource(R.string.errors), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.errors), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.errorCount > 0) Color.Yellow else Color.Green - ) - - Text( - text = "${countToHumanReadable(item.errorCount)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1f), - color = MaterialTheme.colors.placeholderText - ) - - Icon( - imageVector = Icons.Default.DeleteSweep, - stringResource(R.string.spam), - modifier = Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.spam), - Toast.LENGTH_SHORT - ) - .show() - } - } - ), - tint = if (item.spamCount > 0) Color.Yellow else Color.Green - ) - - Text( - text = "${countToHumanReadable(item.spamCount)}", - maxLines = 1, - fontSize = 12.sp, - modifier = Modifier.weight(1f), - color = MaterialTheme.colors.placeholderText - ) - } - } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat.fillMaxWidth() + ) { + RenderStatusRow( + item = item, + onToggleDownload = onToggleDownload, + onToggleUpload = onToggleUpload, + modifier = HalfStartPadding.weight(1f) + ) } } } @@ -673,6 +410,384 @@ fun ServerConfig( } } +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun RenderStatusRow( + item: RelaySetupInfo, + onToggleDownload: (RelaySetupInfo) -> Unit, + onToggleUpload: (RelaySetupInfo) -> Unit, + modifier: Modifier +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Icon( + imageVector = Icons.Default.Download, + contentDescription = stringResource(R.string.read_from_relay), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { onToggleDownload(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.read_from_relay), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.read) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + + Text( + text = countToHumanReadableBytes(item.downloadCountInBytes), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.Upload, + stringResource(R.string.write_to_relay), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { onToggleUpload(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.write_to_relay), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.write) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + + Text( + text = countToHumanReadableBytes(item.uploadCountInBytes), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.SyncProblem, + stringResource(R.string.errors), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.errors), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.errorCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor + ) + + Text( + text = countToHumanReadable(item.errorCount, "errors"), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) + + Icon( + imageVector = Icons.Default.DeleteSweep, + stringResource(R.string.spam), + modifier = Modifier + .size(15.dp) + .combinedClickable( + onClick = { }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.spam), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.spamCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor + ) + + Text( + text = countToHumanReadable(item.spamCount, "spam"), + maxLines = 1, + fontSize = 12.sp, + modifier = modifier, + color = MaterialTheme.colors.placeholderText + ) +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun RenderActiveToggles( + item: RelaySetupInfo, + onToggleFollows: (RelaySetupInfo) -> Unit, + onTogglePrivateDMs: (RelaySetupInfo) -> Unit, + onTogglePublicChats: (RelaySetupInfo) -> Unit, + onToggleGlobal: (RelaySetupInfo) -> Unit, + onToggleSearch: (RelaySetupInfo) -> Unit +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Text( + text = stringResource(id = R.string.active_for), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.placeholderText, + modifier = Modifier.padding(start = 2.dp, end = 5.dp), + fontSize = 14.sp + ) + + IconButton( + modifier = Size30Modifier, + onClick = { onToggleFollows(item) } + ) { + Icon( + painterResource(R.drawable.ic_home), + stringResource(R.string.home_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleFollows(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.home_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onTogglePrivateDMs(item) } + ) { + Icon( + painterResource(R.drawable.ic_dm), + stringResource(R.string.private_message_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onTogglePrivateDMs(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.private_message_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onTogglePublicChats(item) } + ) { + Icon( + imageVector = Icons.Default.Groups, + contentDescription = stringResource(R.string.public_chat_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onTogglePublicChats(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.public_chat_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + IconButton( + modifier = Size30Modifier, + onClick = { onToggleGlobal(item) } + ) { + Icon( + imageVector = Icons.Default.Public, + stringResource(R.string.global_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleGlobal(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.global_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.GLOBAL)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } + + IconButton( + modifier = Size30Modifier, + onClick = { onToggleSearch(item) } + ) { + Icon( + imageVector = Icons.Default.Search, + stringResource(R.string.search_feed), + modifier = Modifier + .padding(horizontal = 5.dp) + .size(15.dp) + .combinedClickable( + onClick = { onToggleSearch(item) }, + onLongClick = { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.search_feed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ), + tint = if (item.feedTypes.contains(FeedType.SEARCH)) { + MaterialTheme.colors.allGoodColor + } else { + MaterialTheme.colors.onSurface.copy( + alpha = 0.32f + ) + } + ) + } +} + +@Composable +private fun FirstLine( + item: RelaySetupInfo, + onClick: () -> Unit, + onDelete: (RelaySetupInfo) -> Unit, + modifier: Modifier +) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { + Text( + text = item.briefInfo.displayUrl, + modifier = Modifier.clickable(onClick = onClick), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (item.paidRelay) { + Icon( + imageVector = Icons.Default.Paid, + null, + modifier = Modifier + .padding(start = 5.dp, top = 1.dp) + .size(14.dp), + tint = MaterialTheme.colors.allGoodColor + ) + } + } + + IconButton( + modifier = Modifier.size(30.dp), + onClick = { onDelete(item) } + ) { + Icon( + imageVector = Icons.Default.Cancel, + null, + modifier = Modifier + .padding(start = 10.dp) + .size(15.dp), + tint = WarningColor + ) + } + } +} + @Composable fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Unit) { var url by remember { mutableStateOf(relayToAdd) } @@ -702,7 +817,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni modifier = Modifier .size(Size35dp) .padding(horizontal = 5.dp), - tint = if (read) Color.Green else MaterialTheme.colors.placeholderText + tint = if (read) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText ) } @@ -713,7 +828,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni modifier = Modifier .size(Size35dp) .padding(horizontal = 5.dp), - tint = if (write) Color.Green else MaterialTheme.colors.placeholderText + tint = if (write) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText ) } @@ -739,9 +854,16 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni } } -fun countToHumanReadable(counter: Int) = when { - counter >= 1000000000 -> "${round(counter / 1000000000f)}G" - counter >= 1000000 -> "${round(counter / 1000000f)}M" - counter >= 1000 -> "${round(counter / 1000f)}k" +fun countToHumanReadableBytes(counter: Int) = when { + counter >= 1000000000 -> "${round(counter / 1000000000f)} GB" + counter >= 1000000 -> "${round(counter / 1000000f)} MB" + counter >= 1000 -> "${round(counter / 1000f)} KB" else -> "$counter" } + +fun countToHumanReadable(counter: Int, str: String) = when { + counter >= 1000000000 -> "${round(counter / 1000000000f)}G $str" + counter >= 1000000 -> "${round(counter / 1000000f)}M $str" + counter >= 1000 -> "${round(counter / 1000f)}K $str" + else -> "$counter $str" +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index ef787703a..885b28b07 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.RelayPool @@ -24,6 +25,7 @@ class NewRelayListViewModel : ViewModel() { fun load(account: Account) { this.account = account clear() + loadRelayDocuments() } fun create() { @@ -36,17 +38,34 @@ class NewRelayListViewModel : ViewModel() { clear() } + fun loadRelayDocuments() { + viewModelScope.launch(Dispatchers.IO) { + _relays.value.forEach { item -> + Nip11CachedRetriever.loadRelayInfo( + dirtyUrl = item.url, + onInfo = { + togglePaidRelay(item, it.limitation?.payment_required ?: false) + }, + onError = { url, errorCode, exceptionMessage -> + } + ) + } + } + } + fun clear() { _relays.update { var relayFile = account.userProfile().latestContactList?.relays() // Ugly, but forces nostr.band as the only search-supporting relay today. // TODO: Remove when search becomes more available. - if (relayFile?.none { it.key == Constants.forcedRelayForSearch.url } == true) { - relayFile = relayFile + Pair( - Constants.forcedRelayForSearch.url, - ContactListEvent.ReadWrite(Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write) - ) + if (relayFile?.none { it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet } == true) { + relayFile = relayFile + Constants.forcedRelayForSearch.map { + Pair( + it.url, + ContactListEvent.ReadWrite(it.read, it.write) + ) + } } if (relayFile != null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt index 952f455e0..9fbaf2463 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context -import android.util.Log -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,27 +25,24 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation -import com.vitorpamplona.amethyst.service.HttpClient -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.components.ClickableEmail import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.note.LoadUser +import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import okhttp3.Call -import okhttp3.Callback -import okhttp3.Request -import okhttp3.Response -import java.io.IOException +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding @OptIn(ExperimentalLayoutApi::class) @Composable fun RelayInformationDialog( onClose: () -> Unit, + relayBriefInfo: RelayBriefInfo, relayInfo: RelayInformation, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -79,18 +73,25 @@ fun RelayInformationDialog( }) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Title(relayInfo.name ?: "") - } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = StdPadding.fillMaxWidth()) { + Column() { + RenderRelayIcon( + relayBriefInfo.favIcon, + Size55dp + ) + } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - SectionContent(relayInfo.description ?: "") + Spacer(modifier = DoubleHorzSpacer) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row() { + Title(relayInfo.name?.trim() ?: "") + } + + Row() { + SubtitleContent(relayInfo.description?.trim() ?: "") + } + } } Section(stringResource(R.string.owner)) @@ -212,7 +213,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) { val text = item.toString().padStart(2, '0') Box(Modifier.padding(10.dp)) { ClickableUrl( - urlText = "$text", + urlText = text, url = "https://github.com/nostr-protocol/nips/blob/master/$text.md" ) } @@ -222,7 +223,7 @@ private fun DisplaySupportedNips(relayInfo: RelayInformation) { val text = item.padStart(2, '0') Box(Modifier.padding(10.dp)) { ClickableUrl( - urlText = "$text", + urlText = text, url = "https://github.com/nostr-protocol/nips/blob/master/$text.md" ) } @@ -258,13 +259,18 @@ private fun DisplayOwnerInformation( @Composable fun Title(text: String) { - Spacer(modifier = DoubleVertSpacer) Text( text = text, fontWeight = FontWeight.Bold, fontSize = 24.sp ) - Spacer(modifier = DoubleVertSpacer) +} + +@Composable +fun SubtitleContent(text: String) { + Text( + text = text + ) } @Composable @@ -285,85 +291,3 @@ fun SectionContent(text: String) { text = text ) } - -fun loadRelayInfo( - dirtyUrl: String, - context: Context, - scope: CoroutineScope, - onInfo: (RelayInformation) -> Unit -) { - try { - val url = if (dirtyUrl.contains("://")) { - dirtyUrl - .replace("wss://", "https://") - .replace("ws://", "http://") - } else { - "https://$dirtyUrl" - } - - val request: Request = Request - .Builder() - .header("Accept", "application/nostr+json") - .url(url) - .build() - - HttpClient.getHttpClient() - .newCall(request) - .enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - checkNotInMainThread() - response.use { - val body = it.body.string() - try { - if (it.isSuccessful) { - onInfo(RelayInformation.fromJson(body)) - } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } catch (e: Exception) { - Log.e("RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - override fun onFailure(call: Call, e: IOException) { - Log.e("RelayInfoFail", "$dirtyUrl unavailable", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } - } - ) - } catch (e: Exception) { - Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.an_error_occurred_trying_to_get_relay_information, dirtyUrl), - Toast.LENGTH_SHORT - ).show() - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index e2d4af042..31028d215 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -29,19 +29,27 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch data class RelayList( val relay: Relay, + val relayInfo: RelayBriefInfo, val isSelected: Boolean ) +data class RelayInfoDialog( + val relayBriefInfo: RelayBriefInfo, + val relayInfo: RelayInformation +) + @Composable fun RelaySelectionDialog( - list: List, + preSelectedList: List, onClose: () -> Unit, onPost: (list: List) -> Unit, accountViewModel: AccountViewModel, @@ -49,34 +57,29 @@ fun RelaySelectionDialog( ) { val scope = rememberCoroutineScope() val context = LocalContext.current - val relayList = accountViewModel.account.activeRelays()?.filter { - it.write - }?.map { - it - } ?: accountViewModel.account.convertLocalRelays().filter { - it.write - } var relays by remember { mutableStateOf( - relayList.map { + accountViewModel.account.activeWriteRelays().map { RelayList( - it, - list.any { relay -> it.url == relay.url } + relay = it, + relayInfo = RelayBriefInfo(it.url), + isSelected = preSelectedList.any { relay -> it.url == relay.url } ) } ) } - var relayInfo: RelayInformation? by remember { mutableStateOf(null) } + var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - if (relayInfo != null) { + relayInfo?.let { RelayInformationDialog( onClose = { relayInfo = null }, - relayInfo = relayInfo!!, - accountViewModel, - nav + relayInfo = it.relayInfo, + relayBriefInfo = it.relayBriefInfo, + accountViewModel = accountViewModel, + nav = nav ) } @@ -115,14 +118,14 @@ fun RelaySelectionDialog( } ) - PostButton( + SaveButton( onPost = { val selectedRelays = relays.filter { it.isSelected } if (selectedRelays.isEmpty()) { scope.launch { Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show() } - return@PostButton + return@SaveButton } onPost(selectedRelays.map { it.relay }) onClose() @@ -153,10 +156,7 @@ fun RelaySelectionDialog( key = { _, item -> item.relay.url } ) { index, item -> RelaySwitch( - text = item.relay.url - .removePrefix("ws://") - .removePrefix("wss://") - .removeSuffix("/"), + text = item.relayInfo.displayUrl, checked = item.isSelected, onClick = { relays = relays.mapIndexed { j, item -> @@ -168,9 +168,30 @@ fun RelaySelectionDialog( } }, onLongPress = { - loadRelayInfo(item.relay.url, context, scope) { - relayInfo = it - } + accountViewModel.retrieveRelayDocument( + item.relay.url, + onInfo = { + relayInfo = RelayInfoDialog(RelayBriefInfo(item.relay.url), it) + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 406eeca5c..a9793d2d4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -451,8 +451,6 @@ fun GenericMainTopBar( nav: (String) -> Unit, content: @Composable (AccountViewModel) -> Unit ) { - val coroutineScope = rememberCoroutineScope() - Column(modifier = BottomTopHeight) { MyTopAppBar( elevation = 0.dp, @@ -476,6 +474,7 @@ fun GenericMainTopBar( } }, navigationIcon = { + val coroutineScope = rememberCoroutineScope() LoggedInUserPictureDrawer(accountViewModel) { coroutineScope.launch { scaffoldState.drawerState.open() @@ -506,10 +505,9 @@ private fun LoggedInUserPictureDrawer( accountViewModel: AccountViewModel, onClick: () -> Unit ) { - val accountUserState by accountViewModel.account.userProfile().live().metadata.observeAsState() + val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState() - val pubkeyHex = remember { accountUserState?.user?.pubkeyHex ?: "" } - val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() } + val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex } IconButton( onClick = onClick diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 0e8f37c8d..c567d0481 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.Send import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -63,7 +64,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.map -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences @@ -72,11 +72,12 @@ import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.note.LoadStatuses -import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog @@ -454,7 +455,6 @@ fun ListContent( } val coroutineScope = rememberCoroutineScope() - val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } var wantsToEditRelays by remember { mutableStateOf(false) } @@ -490,7 +490,7 @@ fun ListContent( ) IconRowRelays( - relayViewModel = relayViewModel, + accountViewModel = accountViewModel, onClick = { coroutineScope.launch { scaffoldState.drawerState.close() @@ -633,27 +633,37 @@ private fun enableTor( } @Composable -private fun RelayStatus( - relayViewModel: RelayPoolViewModel -) { - val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--") - val isConnected by relayViewModel.isConnected.observeAsState(false) +private fun RelayStatus(accountViewModel: AccountViewModel) { + val connectedRelaysText by RelayPool.statusFlow.collectAsState(initial = RelayPoolStatus(0, 0)) - RenderRelayStatus(connectedRelaysText, isConnected) + RenderRelayStatus(connectedRelaysText) } @Composable private fun RenderRelayStatus( - connectedRelaysText: String, - isConnected: Boolean + relayPool: RelayPoolStatus ) { + val text by remember(relayPool) { + derivedStateOf { + "${relayPool.connected}/${relayPool.available}" + } + } + + val placeHolder = MaterialTheme.colors.placeholderText + + val color by remember(relayPool) { + derivedStateOf { + if (relayPool.isConnected) { + placeHolder + } else { + Color.Red + } + } + } + Text( - text = connectedRelaysText, - color = if (isConnected) { - MaterialTheme.colors.placeholderText - } else { - Color.Red - }, + text = text, + color = color, style = MaterialTheme.typography.subtitle1 ) } @@ -709,7 +719,7 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl } @Composable -fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) { +fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -736,7 +746,7 @@ fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) { Spacer(modifier = Modifier.width(Size16dp)) - RelayStatus(relayViewModel = relayViewModel) + RelayStatus(accountViewModel = accountViewModel) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index ab4f194c8..5d535c97c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.OnlineChecker @@ -1663,9 +1664,9 @@ fun DisplayRelaySet( ) { val noteEvent = baseNote.event as? RelaySetEvent ?: return - val relays by remember { - mutableStateOf>( - noteEvent.relays().toImmutableList() + val relays by remember(baseNote) { + mutableStateOf( + noteEvent.relays().map { RelayBriefInfo(it) }.toImmutableList() ) } @@ -1720,7 +1721,7 @@ fun DisplayRelaySet( toMembersShow.forEach { relay -> Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) { Text( - relay.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/"), + text = relay.displayUrl, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -1730,7 +1731,7 @@ fun DisplayRelaySet( ) Column(modifier = Modifier.padding(start = 10.dp)) { - RelayOptionsAction(relay, accountViewModel, nav) + RelayOptionsAction(relay.url, accountViewModel, nav) } } } @@ -2436,40 +2437,41 @@ private fun ReplyRow( } } + val showChannelInfo by remember(note) { + derivedStateOf { + if (noteEvent is ChannelMessageEvent || noteEvent is LiveActivitiesChatMessageEvent) { + note.channelHex() + } else { + null + } + } + } + + showChannelInfo?.let { + ChannelHeader( + channelHex = it, + showVideo = false, + showBottomDiviser = false, + sendToChannel = true, + modifier = MaterialTheme.colors.replyModifier.padding(10.dp), + accountViewModel = accountViewModel, + nav = nav + ) + } + if (showReply) { val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } } if (replyingDirectlyTo != null && unPackReply) { ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav) Spacer(modifier = StdVertSpacer) - } else { - // ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav) - } - } else { - val showChannelReply by remember { - derivedStateOf { - (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) || - (noteEvent is LiveActivitiesChatMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) + } else if (showChannelInfo != null) { + val replies = remember { note.replyTo?.toImmutableList() } + val mentions = remember { + (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() + ?: persistentListOf() } - } - if (showChannelReply) { - val channelHex = note.channelHex() - channelHex?.let { - ChannelHeader( - channelHex = channelHex, - showVideo = false, - showBottomDiviser = false, - sendToChannel = true, - modifier = remember { Modifier.padding(vertical = 5.dp) }, - accountViewModel = accountViewModel, - nav = nav - ) - - val replies = remember { note.replyTo?.toImmutableList() } - val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() } - - ReplyInformationChannel(replies, mentions, accountViewModel, nav) - } + ReplyInformationChannel(replies, mentions, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 1796f8171..58bac1021 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -11,17 +11,16 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer @@ -29,80 +28,40 @@ import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifie import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable public fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { var expanded by remember { mutableStateOf(false) } - var showShowMore by remember { mutableStateOf(false) } - var lazyRelayList by remember { - val baseNumber = baseNote.relays.map { - it.removePrefix("wss://").removePrefix("ws://") - }.toImmutableList() - - mutableStateOf(baseNumber) - } - var shortRelayList by remember { - mutableStateOf(lazyRelayList.take(3).toImmutableList()) - } - - val scope = rememberCoroutineScope() - - WatchRelayLists(baseNote) { relayList -> - if (!equalImmutableLists(relayList, lazyRelayList)) { - scope.launch(Dispatchers.Main) { - lazyRelayList = relayList - shortRelayList = relayList.take(3).toImmutableList() - } - } - - val nextShowMore = relayList.size > 3 - if (nextShowMore != showShowMore) { - scope.launch(Dispatchers.Main) { - // only triggers recomposition when actually different - showShowMore = nextShowMore - } + val relayList by baseNote.live().relayInfo.observeAsState(persistentListOf()) + val shortRelayList by remember { + derivedStateOf { + relayList.take(3).toImmutableList() } } Spacer(DoubleVertSpacer) if (expanded) { - VerticalRelayPanelWithFlow(lazyRelayList, accountViewModel, nav) + VerticalRelayPanelWithFlow(relayList, accountViewModel, nav) } else { VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav) } - if (showShowMore && !expanded) { + if (relayList.size > 3 && !expanded) { ShowMoreRelaysButton { expanded = true } } } -@Composable -private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList) -> Unit) { - val noteRelaysState by baseNote.live().relays.observeAsState() - - LaunchedEffect(key1 = noteRelaysState) { - launch(Dispatchers.IO) { - val relayList = noteRelaysState?.note?.relays?.map { - it.removePrefix("wss://").removePrefix("ws://") - } ?: emptyList() - - onListChanges(relayList.toImmutableList()) - } - } -} - @OptIn(ExperimentalLayoutApi::class) @Composable @Stable private fun VerticalRelayPanelWithFlow( - relays: ImmutableList, + relays: ImmutableList, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 4c738c091..c0a97c319 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.note +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -17,7 +18,6 @@ import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -29,13 +29,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter @@ -44,6 +46,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size15dp import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch @Composable public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { @@ -59,15 +63,13 @@ public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewMo @OptIn(ExperimentalLayoutApi::class) @Composable fun RenderRelayList(baseNote: Note, expanded: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - val noteRelays by baseNote.live().relays.map { - it.note.relays - }.observeAsState(baseNote.relays) + val noteRelays by baseNote.live().relayInfo.observeAsState() FlowRow(StdStartPadding) { val relaysToDisplay = remember(noteRelays, expanded.value) { - if (expanded.value) noteRelays else noteRelays.take(3) + if (expanded.value) noteRelays else noteRelays?.take(3)?.toImmutableList() } - relaysToDisplay.forEach { + relaysToDisplay?.forEach { RenderRelay(it, accountViewModel, nav) } } @@ -104,14 +106,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) { } @Composable -fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - val iconUrl by remember(dirtyUrl) { - derivedStateOf { - val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/") - "https://$cleanUrl/favicon.ico" - } - } - +fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: (String) -> Unit) { var relayInfo: RelayInformation? by remember { mutableStateOf(null) } if (relayInfo != null) { @@ -120,8 +115,9 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri relayInfo = null }, relayInfo = relayInfo!!, - accountViewModel, - nav + relayBriefInfo = relay, + accountViewModel = accountViewModel, + nav = nav ) } @@ -130,7 +126,7 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri val interactionSource = remember { MutableInteractionSource() } val ripple = rememberRipple(bounded = false, radius = Size15dp) - val clickableModifier = remember(dirtyUrl) { + val clickableModifier = remember(relay) { Modifier .padding(1.dp) .size(Size15dp) @@ -139,9 +135,30 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri interactionSource = interactionSource, indication = ripple, onClick = { - loadRelayInfo(dirtyUrl, context, scope) { - relayInfo = it - } + accountViewModel.retrieveRelayDocument( + relay.url, + onInfo = { + relayInfo = it + }, + onError = { url, errorCode, exceptionMessage -> + val msg = when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) + } + + scope.launch { + Toast + .makeText( + context, + msg, + Toast.LENGTH_SHORT + ) + .show() + } + } + ) } ) } @@ -149,17 +166,17 @@ fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (Stri Box( modifier = clickableModifier ) { - RenderRelayIcon(iconUrl) + RenderRelayIcon(relay.favIcon) } } @Composable -private fun RenderRelayIcon(iconUrl: String) { +fun RenderRelayIcon(iconUrl: String, size: Dp = Size13dp) { val backgroundColor = MaterialTheme.colors.background val iconModifier = remember { Modifier - .size(Size13dp) + .size(size) .clip(shape = CircleShape) .background(backgroundColor) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayPoolViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayPoolViewModel.kt deleted file mode 100644 index a57974b6c..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayPoolViewModel.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.vitorpamplona.amethyst.ui.screen - -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map -import com.vitorpamplona.amethyst.service.relays.RelayPool - -@Stable -class RelayPoolViewModel : ViewModel() { - val connectionStatus = RelayPool.live.map { - val connectedRelays = it.relays.connectedRelays() - val availableRelays = it.relays.availableRelays() - "$connectedRelays/$availableRelays" - }.distinctUntilChanged() - - val isConnected = RelayPool.live.map { - it.relays.connectedRelays() > 0 - }.distinctUntilChanged() -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index e012b8e1c..53b339c8c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -19,10 +19,13 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.Nip05Verifier +import com.vitorpamplona.amethyst.service.Nip11CachedRetriever +import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.components.UrlPreviewState @@ -571,6 +574,16 @@ class AccountViewModel(val account: Account) : ViewModel() { } } + fun retrieveRelayDocument( + dirtyUrl: String, + onInfo: (RelayInformation) -> Unit, + onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + Nip11CachedRetriever.loadRelayInfo(dirtyUrl, onInfo, onError) + } + } + class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { return AccountViewModel(account) as AccountViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 615b06c11..9073c78ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -416,14 +416,11 @@ private fun VideoUserOptionAction( @OptIn(ExperimentalLayoutApi::class) @Composable private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - val noteRelaysState by baseNote.live().relays.observeAsState() - val noteRelays = remember(noteRelaysState) { - noteRelaysState?.note?.relays ?: emptySet() - } + val noteRelays by baseNote.live().relayInfo.observeAsState() FlowRow() { - noteRelays.forEach { dirtyUrl -> - RenderRelay(dirtyUrl, accountViewModel, nav) + noteRelays?.forEach { relayInfo -> + RenderRelay(relayInfo, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index e4b1d22bb..538442536 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -24,3 +24,9 @@ val DarkerGreen = Color.Green.copy(alpha = 0.32f) val WarningColor = Color(0xFFC62828) val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) + +val LightWarningColor = Color(0xFFffcc00) +val DarkWarningColor = Color(0xFFF8DE22) + +val LightAllGoodColor = Color(0xFF339900) +val DarkAllGoodColor = Color(0xFF99cc33) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 42a6331c6..541420948 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -286,6 +286,12 @@ val Colors.overPictureBackground: Color val Colors.bitcoinColor: Color get() = if (isLight) BitcoinLight else BitcoinDark +val Colors.warningColor: Color + get() = if (isLight) LightWarningColor else DarkWarningColor + +val Colors.allGoodColor: Color + get() = if (isLight) LightAllGoodColor else DarkAllGoodColor + val Colors.markdownStyle: RichTextStyle get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1afc75465..cbfbb9564 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -37,7 +37,8 @@ Přihlaste se pomocí soukromého klíče, abyste mohli zrušit sledování Zapy Počet zobrazení - boostováno + Zvýšení + zvýšeno Citovat Nová částka v sats Přidat @@ -290,6 +291,7 @@ Odesílatel a příjemce si mohou navzájem vidět a číst zprávu Anonymní Příjemce a veřejnost neví, kdo platbu poslal + Né Zap Žádná stopa v Nostr, pouze v Lightning Souborový server LnAddress nebo @Uživatel @@ -358,6 +360,7 @@ Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob Nový symbol reakce Nebyly vybrány žádné typy reakcí. Dlouhým stiskem změňte + Zap-sběr Přidá cílovou částku sats pro tento příspěvek. Podporované klienty ji mohou zobrazovat jako ukazatel postupu k podněcování darování Cílová částka v sats Zapraiser na %1$s. Do cíle zbývá %2$s sats @@ -451,4 +454,13 @@ Automaticky přehrávat videa a GIFy Zobrazit náhledy URL Kdy načíst obrázek + Kopírovat URL do schránky + Kopírovat ID poznámky do schránky + Vytvořeno + Pravidla + Aktualizovat svůj stav + Chyba při zpracování chybové zprávy + Hlasy jsou váženy podle hodnoty zapu. Můžete nastavit minimální částku, abyste se vyhnuli spammerům, a maximální částku, abyste zabránili velkým zapperům, kteří by mohli ovládnout hlasování. Použijte stejnou částku v obou polích, aby byla hodnota každého hlasu stejná. Nechte prázdné pro přijetí libovolné částky. + Nelze odeslat zap + Poslat zprávu uživateli diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8c77819a1..91e737c26 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,6 +36,7 @@ Melden Sie sich mit einem privaten Schlüssel an, um zu folgen Melden Sie sich mit einem privaten Schlüssel an, um nicht mehr zu folgen Aufrufe + Verstärken Verstärkt Zitat Neuer Betrag in Sats @@ -169,6 +170,8 @@ anz der Bedingungen ist erforderlich Nostr-Adresse nie jetzt + s + t Nacktheit Beleidigungen / Hassrede Hassrede melden @@ -246,8 +249,11 @@ anz der Bedingungen ist erforderlich Zu den öffentlichen Lesezeichen hinzufügen Aus den privaten Lesezeichen entfernen Aus den öffentlichen Lesezeichen entfernen + Wallet-Verbindungsdienst Autorisiert einen Nostr Secret, Zaps ohne Verlassen der App zu bezahlen. Bewahren Sie den geheimen Schlüssel sicher auf und verwenden Sie nach Möglichkeit ein privates Relay Wallet Connect Public Key + Wallet-Verbindungs-Relay + Wallet-Verbindungs-Geheimnis Geheimen Schlüssel anzeigen nsec / hexadezimaler privater Schlüssel Spendenbetrag in Sats @@ -288,6 +294,7 @@ anz der Bedingungen ist erforderlich Absender und Empfänger können einander sehen und die Nachricht lesen Anonym Empfänger und die Öffentlichkeit wissen nicht, wer die Zahlung gesendet hat + Keine Zap Keine Spur in Nostr, nur in Lightning Dateiserver LnAddress oder @Benutzer @@ -321,6 +328,7 @@ anz der Bedingungen ist erforderlich Nein Folgen-Liste Alle Folgen + Weltweit ## Über Tor mit Orbot verbinden \n\n1. Installiere [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) \n2. Starte Orbot @@ -356,6 +364,7 @@ anz der Bedingungen ist erforderlich Warnung bei Meldungen von deinen Abonnements Neues Reaktionssymbol Keine Reaktionstypen ausgewählt. Lange drücken, um zu ändern + Zap-Sammlung Fügt einen Zielbetrag in Sats für diesen Beitrag hinzu. Unterstützende Clients können dies als Fortschrittsbalken anzeigen, um Spenden zu fördern Zielbetrag in Sats Zapraiser bei %1$s. %2$s Sats bis zum Ziel @@ -443,4 +452,13 @@ anz der Bedingungen ist erforderlich Videos und GIFs automatisch abspielen URL-Vorschauen anzeigen Wann Bilder geladen werden sollen + URL in die Zwischenablage kopieren + Notiz-ID in die Zwischenablage kopieren + Erstellt am + Regeln + Status aktualisieren + Fehler beim Verarbeiten der Fehlermeldung + Die Abstimmungen werden nach der Höhe des Zaps gewichtet. Sie können einen Mindestbetrag festlegen, um Spam zu verhindern, und einen Höchstbetrag, um zu verhindern, dass große Zapper die Abstimmung dominieren. Verwenden Sie denselben Betrag in beiden Feldern, um sicherzustellen, dass jeder Stimme der gleiche Wert zukommt. Lassen Sie es leer, um jeden Betrag zu akzeptieren. + Zap konnte nicht gesendet werden + Mit dem Benutzer kommunizieren diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 000000000..8209fb01d --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,530 @@ + + + Arahkan ke Kode QR + Tampilkan QR + Foto Profil + Pindai QR + Tampilkan Saja + Postingan dilaporkan oleh + Data sedang dimuat atau tidak dapat ditemukan di daftar relai Anda + Gambar Kanal + Data yang direferensikan tidak ditemukan + Tidak dapat mendeskripsikan pesan + Gambar Grup + Konten Eksplisit + Spam + Peniruan/penipuan + Perilaku Ilegal + Tidak dikenal + Ikon Relai + Penulis Tidak Dikenal + Salin Teks + Salin ID Penulis (Author ID) + Salin ID Tulisan (Note ID) + Siarkan + Minta Penghapusan + Blokir / Laporkan + + Laporkan Spam / Penipuan + Laporkan Peniruan + Laporkan Konten Eksplisit + Laporkan Perilaku Ilegal + Masuk dengan Kunci pribadi (nsec) untuk dapat membalas tulisan + Masuk dengan Kunci pribadi (nsec) untuk dapat mempromosikan tulisan + Masuk dengan Kunci pribadi (nsec) untuk dapat menyukai tulisan + Tidak Ada Pengaturan Jumlah Zap. Tekan dan tahan tombol untuk mengubah + Masuk dengan Kunci pribadi (nsec) untuk dapat mengirim Zaps + Masuk dengan Kunci pribadi (nsec) untuk dapat mengikuti + Masuk dengan Kunci pribadi (nsec) untuk berhenti mengikuti + Zaps + Jumlah pengamat + Promosi + dipromosikan + Kutipan + Jumlah Baru dalam Sats + Tambah + "membalas kepada " + " dan " + dalam kanal + Spanduk Profil + Pembayaran Berhasil + Gagal menguraikan pesan kesalahan + " Mengikuti" + " Pengikut" + Profil + Filter Keamanan + Keluar + Tampilkan lebih banyak + Faktur Tagihan Lightning + Bayar + Lightning Tip + Catatan ke Penerima + Terima kasih banyak! + Jumlah dalam Sats + Kirim Sats + Gagal menampilkan pratinjau untuk %1$s : %2$s + Pratinjau Gambar Kartu untuk %1$s + Kanal Baru + Nama Kanal + Grupku yang Hebat + Alamat Url Gambar + Deskripsi + "Tentang kami.. " + Apa yang anda pikirkan? + Kirim + Simpan + Buat + Batal + Gagal mengunggah gambar + Alamat Relai + Kiriman + Bytes + Errors + Beranda + Area Pesan Pribadi + Area Percakapan Publik + Area Global + Area Pencarian + Tambah Relai + Nama ditampilkan + Namaku yg ditampilkan + Nama pengguna + Nama penggunaku + Tentang saya + URL Avatar + "URL Spanduk/Banner " + URL Situs Web + Alamat LN + LN URL (versi lama) + Gambar disimpan ke galeri + Gagal menyimpan gambar + Unggah Gambar + Mengunggah… + Pengguna tidak memiliki alamat lightning (LN) yang diatur untuk menerima sats + "balas di sini.. " + Menyalin ID Catatan ke papan klip untuk dibagikan di Nostr + Salin ID Kanal (Catatan) ke Papan Klip + Ubah Metadata Kanal + Gabung + Dikenali + Permintaan Baru + Pengguna diblokir + Utas Baru + Percakapan + Catatan + Balasan + Mengikuti + Laporan + Lebih banyak pilihan + " Relai" + Situs Web + Alamat Lightning + Salin Nsec ID (kata sandi anda) ke papan klip sebagai cadangan + Salin Kunci Privat ke papan klip + Salin Kunci Publik ke papan klip untuk dibagikan + Salin Kunci Publik (NPub) ke papan klip + Kirim Pesan Langsung + Ubah Metadata Pengguna + Ikuti + Ikuti balik + Batalkan blokir + Salin ID Pengguna + Batalkan blokir pengguna + npub, nama pengguna, teks + Bersihkan + Logo Aplikasi + nsec.. or npub.. + Tampilkan Kata Sandi + Sembunyikan Kata Sandi + Kunci tidak sah + Saya menerima + syarat penggunaan + Penerimaan persyaratan diperlukan + Kunci diperlukan + Masuk + Buat kunci baru + Memuat data umpan + "Gagal memuat balasan: " + Coba lagi + Data umpan kosong. + Perbarui + dibuat + "dengan deskripsi " + dan gambar + mengubah nama percakapan menjadi + deskripsi untuk + dan gambar untuk + Tinggalkan + Berhenti mengikuti + Kanal dibuat + Informasi kanal diubah menjadi + Percakapan Publik + kiriman diterima + Hapus + Otomatis + diterjemahkan dari + ke + Tampilkan dalam %1$s (prioritas) + Selalu terjemahkan ke %1$s + Jangan terjemahkan dari %1$s + Alamat Nostr + tidak pernah + sekarang + jam + menit + hari + Ketelanjangan + Ujaran kotor/kebencian + Laporkan ujaran kebencian + Laporkan Ketelanjangan / Pornografi + lainnya + Tandai semua yg dikenal telah dibaca + Tandai pesan baru telah dibaca + Tandai semua telah dibaca + Cadangkan Kunci + "\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 " + Kunci rahasia (nsec) disalin ke clipboard + Salin kunci rahasia saya + Autentikasi gagal + Kesalahan + Dibuat oleh %1$s + Gambar penghargaan lencana untuk %1$s + Anda Menerima Penghargaan Lencana baru + Penghargaan lencana diberikan kepada + Teks catatan disalin ke papan klip + \@npub Penulis disalin ke papan klip + Note ID (@note1) disalin ke papan klip + Pilih Teks + <Gagal mendeskripsikan pesan pribadi>\n\nAnda dikutip dalam percakapan pribadi/terenkripsi antara %1$s dan %2$s. + Tambah Akun Baru + Akun + Pilih Akun + Tambah Akun Baru + Akun Aktif + Memiliki kunci pribadi + Baca saja, tidak ada kunci pribadi + Kembali + Pilih + Bagikan Tauatan ke Peramban + Bagikan + ID Penulis + ID Catatan + Salin Teks + Hapus + Berhenti mengikuti + Ikuti + Meminta Penghapusan + Amethyst akan meminta agar catatan Anda dihapus dari relai yang saat ini Anda sambungkan. Tidak ada jaminan bahwa catatan Anda akan dihapus secara permanen dari relai tersebut, atau dari relai lain tempat catatan itu disimpan. + Blokir + Hapus + Blokir + Laporkan + Hapus + Jangan tampilkan lagi + Spam atau penipuan + Perilaku tidak senonoh atau penuh kebencian + Peniruan identitas yang berbahaya + Konten ketelanjangan atau grafis + Perilaku Ilegal + Memblokir pengguna akan menyembunyikan konten mereka di aplikasi Anda. Catatan Anda masih dapat dilihat oleh publik, termasuk oleh orang yang Anda blokir. Pengguna yang diblokir terdaftar di layar Filter Keamanan. + + Laporkan Penyalahgunaan + Semua laporan yang diposting akan terlihat oleh publik. + Secara opsional, berikan konteks tambahan tentang laporan Anda… + Konteks Tambahan + Alasan + Pilih alasan… + Kirim Laporan + Blokir dan Laporkan + Blokir + + Penanda + Penanda Pribadi + Penanda Publik + Tambahkan Penanda Pribadi + Tambahkan Penanda Publik + Hapus dari Penanda Pribadi + Hapus dari Penanda Publik + + Layanan Wallet Connect + Mengijinkan Kunci Rahasia Nostr untuk membayar zaps tanpa meninggalkan aplikasi. Jaga kunci tetap aman dan gunakan relai pribadi jika memungkinkan + Kunci Publik Wallet Connect + Relai Wallet Connect + Kunci Rahasia Wallet Connect + Tampilkan kunci rahasia + nsec / hex (kunci pribadi) + + Jumlah Dijanjikan dalam Sats + Kirim Pemungutan Suara + Bidang yang wajib diisi: + Penerima Zap + Deskripsi Utama Pemungutan Suara… + Pilihan %s + Deskripsi pilihan pemungutan suara + Bidang pilihan (tidak wajib): + Zap terendah + Zap tertinggi + Konsensus + (0–100)% + Berakhir pada + hari + Pemungutan suara ditutup untuk voting baru + Jumlah Zap + Hanya satu suara per pengguna yang diperbolehkan pada jenis pemungutan suara ini + + Mencari data event %1$s + + Tambah pesan publik + Tambah pesan pribadi + Tambah pesan faktur tagihan + + Terima kasih atas semua pekerjaan Anda! + + Buat dan Tambah + Pembuat Pemungutan Suara tidak dapat memberikan suara dalam pemungutan suara sendiri. + + Konten ini sama sejak kiriman tersebut dibuat + Konten ini telah berubah. Penulis mungkin tidak melihat atau menyetujui perubahan tersebut + + Tambah Gambar + Tambah Video + Tambah Dokumen + + Tambahkan ke Pesan + Deskripsi Konten + Perahu berwarna biru di pantai berpasir putih saat matahari terbenam + + Tipe Zap + Tipe Zap untuk semua pilihan + + Publik + Semua orang dapat melihat transaksi dan pesannya + + Pribadi + Pengirim dan Penerima dapat saling melihat dan membaca pesan + + Anonim + Penerima dan semua orang tidak dapat mengetahui siapa pengirim pembayaran + + Bukan Zap + Tidak ada jejak di Nostr, hanya di Lightning Network + + + Server Berkas + Alamat LN atau @Pengguna + + imgur.com - terpercaya + Imgur dapat memodifikasi berkas + + nostrimg.com - terpercaya + NostrImg dapat memodifikasi berkas + + nostr.build - terpercaya + Nostr.build dapat memodifikasi berkas + + nostrfiles.dev - terpercaya + Nostrfiles.dev dapat memodifikasi berkas + + nostrcheck.me - terpercaya + nostrcheck.me dapat memodifikasi berkas + + + Imgur yg dapat diverifikasi (NIP-94) + Periksa jika Imgur mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung + + NostrImg yg dapat diverifikasi (NIP-94) + Periksa jika NostrImg mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung + + Nostr.build yg dapat diverifikasi (NIP-94) + Periksa jika Nostr.build mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung + + Nostrfiles.dev yg dapat diverifikasi (NIP-94) + Periksa jika Nostrfiles.dev mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung + + Nostrcheck.me yg dapat diverifikasi (NIP-94) + Periksa jika Nostrcheck.me mengubah berkas. NIP baru: aplikasi Nostr lain mungkin belum mendukung + + Relai anda (NIP-95) + Berkas disimpan di relai anda. NIP baru: periksa apakah relai mendukung fitur + + Pengaturan Tor/Orbot + Hubungkan melalui pengaturan Orbot Anda + + Putuskan sambungan dari Orbot/Tor Anda? + Data Anda akan segera ditransfer di jaringan reguler (bukan TOR) + Ya + Tidak + + + Daftar yang Diikuti + Semua yang diikuti + Global + "\n ## Terhubung melalui Tor menggunakan Orbot\n \n\n1. Pasang [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)\n \n2. Jalankan Orbot\n \n3. Di dalam Orbot, periksa nomor port Socks. Secara bawaan menggunakan nomor 9050\n \n4. Jika diperlukan, ubah nomor port Orbot\n \n5. Atur nomor port Socks pada layar ini\n \n6. Tekan tombol Aktifkan untuk menggunakan Orbot sebagai proxy\n " + Port Orbot Socks + Nomor port tidak sah + Gunakan Orbot + Putuskan Tor/Orbot + + Pesan Pribadi + Memberi tahu Anda ketika pesan pribadi tiba + + Zaps diterima + Memberi tahu Anda ketika seseorang mengirimkan anda zap + %1$s sats + Dari %1$s + untuk %1$s + + "Beritahu: " + + Gabung Percakapan + ID Pengguna atau Grup + npub, nevent atau kode heksa + Buat + Gabung + + Hari ini + + Peringatan konten + Postingan ini berisi konten sensitif yang mungkin dianggap menyinggung atau mengganggu oleh sebagian orang + Selalu sembunyikan konten sensitif + Selalu tampilkan konten sensitif + Selalu tampilkan peringatan konten + + "Rekomendasi: " + Saring spam dari orang asing (tak diikuti) + Peringatkan ketika kiriman memiliki laporan dari pengikut Anda + + Simbol Reaksi Baru + Tidak ada jenis reaksi yang dipilih. Tekan dan tahan untuk mengubah + + Galang Dana Zap + Menambahkan jumlah target sats yang ingin dikumpulkan untuk kiriman ini. Klien yang mendukung mungkin menunjukkan hal ini sebagai indikator kemajuan dalam memberi insentif pada donasi + Jumlah Target dalam Sats + + Galang Dana Zap mencapai %1$s. %2$s sats untuk memenuhi target + Baca dari Relai + Tulis ke Relai + Terjadi kesalahan saat mencoba mendapatkan informasi relai %1$s + Pemilik + Versi + Perangkat Lunak + Kontak + NIP yg didukung + Biaya Pendaftaran + Alamat Pembayaran + Batasan + Negara + Bahasa + Tags + Kebijakan kiriman + Panjang pesan + Langganan + Penyaringan + Panjang ID Subscription + Awalan minimal + Jumlah Tags maksimum + Panjang konten + PoW terendah + Autentikasi + Pembayaran + Token Cashu + Tukarkan + Tidak ada alamat Lightning yang diatur + Token disalin ke papan klip + + DARING (ON) + LURING (OFF) + BERAKHIR + DIJADWALKAN + + Siaran langsung sedang luring + Siaran langsung berakhir + Keluar akan menghapus semua informasi lokal Anda. Pastikan kunci pribadi Anda dicadangkan untuk menghindari kehilangan akun Anda. Apakah Anda ingin melanjutkan? + Tags yg diikuti + + Relai + + Siaran Langsung + Komunitas + Percakapan + Kiriman yg disetujui + + Grup ini tidak memiliki deskripsi atau aturan. Bicaralah dengan pemiliknya untuk menambahkannya + Komunitas ini tidak memiliki deskripsi. Bicaralah dengan pemiliknya untuk menambahkannya + + Konten Sensitif + Menambahkan peringatan konten sensitif sebelum menampilkan konten ini + Pengaturan + Selalu + Hanya Wifi + Tidak Pernah + + Sistem + Terang + Gelap + Pengaturan Aplikasi + Bahasa + Tema + Pratinjau Gambar + Pemutaran Video + Pratinjau URL + Muat Gambar + + Pengirim Spam + + Diredam. Klik untuk bersuara + Bersuara. Kilik untuk meredam + Cari catatan lokal dan jarak jauh + + Alamat Nostr diverifikasi + Alamat Nostr gagal diverifikasi + Memeriksa Alamat Nostr + Pilih / Hapus Semua + Bawaan + Pilih Relai untuk melanjutkan + + Teruskan Zaps ke: + Klien yg mendukung akan meneruskan zaps ke Alamat LN atau Profil Pengguna di bawah ini, bukan ke alamat LN Anda + + Paparkan Lokasi sebagai + Menambahkan Geohash lokasi Anda ke postingan. Publik akan mengetahui bahwa Anda berada dalam jarak 5 km (3 mil) dari lokasi saat ini + + Menambahkan peringatan konten sensitif sebelum menampilkan konten Anda. Ini ideal untuk konten NSFW apa pun atau konten yang mungkin dianggap menyinggung atau mengganggu oleh sebagian orang + + Fitur Baru + Mengaktifkan mode ini mengharuskan Amethyst mengirim pesan NIP-24 (GiftWrapped, Sealed Direct, dan Group Messages). NIP-24 masih baru dan sebagian besar klien belum menerapkannya. Pastikan penerima menggunakan klien yang kompatibel. + Aktifkan + + Publik + Pribadi + Ke + Subjek + Topik percakapan + \@Pengguna1, @Pengguna2, @Pengguna3 + + Anggota grup ini + Penjelasan kepada anggota + Mengubah nama untuk tujuan baru. + + Untuk Antarmuka Aplikasi + Tema Gelap, Terang, atau Sistem + Memuat gambar dan GIF secara otomatis + Memutar video dan GIF secara otomatis + Tampilkan pratinjau URL + Kapan memuat gambar + + Salin URL ke papan klip + Salin ID Catatan ke papan klip + + Dibuat pada + Aturan + + Perbaharui statusmu + + Kesalahan menguraikan pesan kesalahan + Suara ditimbang berdasarkan jumlah zap. Anda dapat menetapkan jumlah minimum untuk menghindari pembuat spam dan jumlah maksimum untuk menghindari pengirim zap besar mengambil alih pemungutan suara. Gunakan jumlah yang sama di kedua kolom untuk memastikan setiap suara bernilai sama. Biarkan kosong untuk menerima jumlah berapa pun. + + Tidak dapat mengirim zap + Kirim Pesan ke Pengguna + Ok + diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index ffd0a955d..fc704e4d8 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -21,6 +21,7 @@ Copia testo Copia ID autore Copia ID nota + Trasmissione Richiedi cancellazione Blocca / Segnala @@ -39,4 +40,101 @@ Visualizzazioni Boost ricondiviso + Cita + Nuovo importo in Sats + Aggiungi + "rispondendo a" + " e " + "nel canale " + Immagine copertina + Pagamento effettuato + Errore durante l\'interpretazione del messaggio d\'errore + " Seguiti" + " Seguaci" + Profilo + Filtri di sicurezza + Esci + Mostra altro + Fattura Lightning + Paga + Mance Lightning + Nota per il Ricevente + Grazie mille! + Importo in Sats + Invia Sats + "Errore nella creazione dell'anteprima di %1$s : %2$s" + "Anteprima immagine per %1$s" + Nuovo canale + Nome canale + Il mio fantastico gruppo + Url foto + Descrizione + "Su di noi... " + A cosa stai pensando? + Post + Salva + Crea + Cancella + Impossibile caricare l\'immagine + Indirizzo Relay + Posts + Bytes + Errori + Home Feed + Lista Messaggi Privati + Lista Chat Pubbliche + Feed Globale + Risultati Ricerca + Aggiungi un Relay + Nome Visualizzato + Il mio nome visualizzato + Username + L\'accettazione dei termini è obbligatoria + La chiave è obbligatoria + Accesso + Genera una nuova chiave + Caricamento del feed + "Errore durante il caricamento delle risposte: " + Prova di nuovo + Il feed è vuoto. + Aggiorna + creato + con la descrizione di + e l\'immagine + modificato il nome della chat in + descrizione per + e immagine per + Esci + Non seguire più + Canale creato + "Informazioni del canale modificate in" + Chat Pubblica + post ricevuti + Cancella + Auto + tradotto da + a + Traduci sempre in %1$s + Non tradurre mai da %1$s + Indirizzo Nostr + mai + adesso + h + m + d + Nuditá + Volgaritá / Linguaggio forte + Segnala linguaggio forte + Segnala Nuditá / Pornografia + altri + Segna tutti i conosciuti come letti + Segna tutti i nuovi come letti + Segna tutti come letti + Esegui backup delle chiavi + ## 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. + diff --git a/app/src/main/res/values-sw-rKE/strings.xml b/app/src/main/res/values-sw-rKE/strings.xml index 66a570486..453d80492 100644 --- a/app/src/main/res/values-sw-rKE/strings.xml +++ b/app/src/main/res/values-sw-rKE/strings.xml @@ -1,2 +1,476 @@ - + + Elekeza kwa Msimbo wa QR + Onyesha QR + Picha ya Wasifu + Changanua QR + Onyesha Hata hivyo + Chapisho liliripotiwa na + Tukio linapakia au halipatikani kwenye orodha yako ya upeanaji + Picha ya Kituo + Tukio linalorejelewa halijapatikana + Haikuweza kusimbua ujumbe + Picha ya Kikundi + Maudhui Dhahiri + Barua taka + Uigaji + Tabia Haramu + Haijulikani + Aikoni ya Relay + Mwandishi asiyejulikana + Nakili Maandishi + Nakili Kitambulisho cha Mwandishi + Nakili Kitambulisho cha Dokezo + Tangaza + Omba Kufutwa + Zuia / Ripoti + + Ripoti Barua Taka / Ulaghai + Ripoti Uigaji + Ripoti Maudhui Dhahiri + Ripoti Tabia Haramu + Ingia kwa kutumia Ufunguo wa Faragha ili uweze kujibu + Ingia kwa kutumia ufunguo wa Faragha ili uweze kuboresha machapisho + Ingia kwa kutumia ufunguo wa Faragha ili kupenda Machapisho + Hakuna Usanidi wa Kiasi cha Zap. Bonyeza kwa Muda mrefu ili kubadilisha + Ingia kwa kutumia ufunguo wa Faragha ili uweze kutuma Zaps + Ingia kwa kutumia Ufunguo wa Faragha ili uweze Kufuata + Ingia kwa kutumia Ufunguo wa Faragha ili uweze Kuacha Kufuata + Zaps + Idadi ya maoni + Kuongeza + imeimarishwa + Nukuu + Kiasi Kipya kwa Sats + Ongeza + "kujibu " + " na " + "katika chaneli " + Bango la Wasifu + Malipo Yamefaulu + Hitilafu katika kuchanganua ujumbe wa hitilafu + " Kufuatia" + " Wafuasi" + Wasifu + Vichujio vya Usalama + Ondoka + Onyesha Zaidi + Ankara ya umeme + Lipa + Vidokezo vya Umeme + Kumbuka kwa Mpokeaji + Asante sana! + Kiasi katika Sats + Tuma Sats + "Hitilafu katika kuchanganua onyesho la kukagua %1$s : %2$s" + "Hakiki Picha ya Kadi ya %1$s" + Kituo Kipya + Kituo Kipya + Kundi Langu la Ajabu + Url ya picha + Maelezo + "Kuhusu sisi.. " + Unafikiria nini? + Tuma + Hifadhi + Unda + Ghairi + Kushindwa kupakia picha + Anwani ya Relay + Machapisho + Bayti + Makosa + Malisho ya Nyumbani + Malisho ya Ujumbe Binafsi + Malisho ya Mazungumzo ya Umma + Malisho ya Kimataifa + Malisho ya Utafutaji + Ongeza Relay + Jina la Kuonyesha + Jina langu la kuonyesha + Jina la mtumiaji + Jina langu la mtumiaji + Kuhusu mimi + URL ya Picha ya Profaili + URL ya Bango + URL ya Tovuti + Anwani ya LN + URL ya LN (imepitwa na wakati) + Picha imesave kwenye galeria + Kushindwa kuhifadhi picha + Pakia Picha + Inapakia… + Mtumiaji hana anwani ya umeme iliyoandaliwa kupokea sati + "jibu hapa.. " + Hunakili Kitambulisho cha Dokezo kwenye ubao wa kunakili ili kushiriki katika Nostr + Nakili Kitambulisho cha Kituo (Kumbuka) kwenye Ubao wa kunakili + Huhariri Metadata ya Kituo + Jiunge + Inajulikana + Maombi Mapya + Watumiaji Waliozuiwa + Nyuzi Mpya + Mazungumzo + Vidokezo + Majibu + "Anafuata" + "Reports" + Chaguo Zaidi + " Relays" + Tovuti + Anwani ya Umeme + Hunakili Kitambulisho cha Nsec (nywila yako) kwenye ubao wa kunakili kwa ajili ya nakala rudufu + Nakili Ufunguo wa Siri kwenye Ubao wa Kunakili + Hunakili ufunguo wa umma kwenye ubao wa kunakili kwa ajili ya kushiriki + Nakili Ufunguo wa Umma (NPub) kwenye Ubao wa Kunakili + Tuma Ujumbe wa Moja kwa Moja + Huhariri Metadata ya Mtumiaji + Fuata + Fuata tena + Fungua + Nakili Kitambulisho cha Mtumiaji + Fungua Mtumiaji + "npub, jina la mtumiaji, maandishi" + Ondoa + Nembo ya Programu + nsec.. au npub.. + Onyesha Nywila + Ficha Nywila + Ufunguo usiofaa + "Nakubali " + masharti ya matumizi + Kukubaliana na masharti ni lazima + Ufunguo unahitajika + Ingia + Tengeneza Ufunguo Mpya + Inapakia malisho + "Kosa katika kupakia majibu: " + Jaribu tena + Malisho ni tupu. + Sasisha + imeundwa + na maelezo ya + na picha + ilisitisha jina la mazungumzo hadi + maelezo ya + na picha ya + Ondoka + Acha kufuata + Kituo kimeundwa + "Maelezo ya Kituo yamebadilika hadi" + Mazungumzo ya Umma + machapisho yamepokelewa + Ondoa + Auto + imetafsiriwa kutoka + kwa + Onyesha kwa %1$s kwanza + Daima itafsiriwe kwa %1$s + Kamwe usitafsiri kutoka %1$s + Anwani ya Nostr + kamwe + sasa + h + m + d + Uchi + Lugha chafu / Maneno yenye chuki + Ripoti Lugha yenye chuki + Ripoti Uchi / Pornografia + wengine + Funga kama Zimejulikana + Funga kama Mpya + Funga kama Zimejulikana + Nakili Nakala za Ufunguo + ## 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. + + Ufunguo wa siri (nsec) umeinakiliwa kwenye ubao wa kunakili + Nakili Ufunguo Wangu wa Siri + Uthibitishaji umeshindwa + Kosa + "Imeundwa na %1$s" + "Picha ya tuzo ya alama kwa %1$s" + Umepokea Tuzo Mpya ya Alama + Tuzo ya alama imetolewa kwa + Maudhui ya dokezo yamenakiliwa kwenye ubao wa kunakili + Imesakinishwa @npub ya mwandishi kwa ubao wa kunakili + Imesakinishwa ID ya andiko (@note1) kwa ubao wa kunakili + Chagua Matini + "<Haiwezi kuifanyia utawala ujumbe binafsi uliofichwa>\n\nUlinukuliwa katika mazungumzo ya faragha/yaliyofichwa kati ya %1$s na %2$s." + Ongeza Akaunti Mpya + Akaunti + Chagua Akaunti + Ongeza Akaunti Mpya + Akaunti Iliyo Hai + Ina ufunguo wa faragha + Soma tu, hakuna ufunguo wa faragha + Rudi + Chagua + Shiriki Kiungo cha Kivinjari + Shiriki + ID ya Mwandishi + ID ya Andiko + Nakili Matini + Futa + Acha Kufuata + Fuata + Omba Kufutwa + Amethyst itaomba kwamba andiko lako lifutwe kutoka kwa relays ambazo umekuwa umeunganishwa. Hakuna dhamana kwamba andiko lako litafutwa kabisa kutoka kwa relays hizo, au kutoka kwa relays nyingine ambapo linaweza kuwa limehifadhiwa. + Zuia + Futa + Zuia + Ripoti + Futa + Usionyeshe Tena + Barua Taka au Udanganyifu + Lugha chafu au tabia yenye chuki + Uigaaji mbaya + Uchi au yaliyomo ya kushangaza + Tabia Haramu + Kuzuia mtumiaji kutaficha maudhui yao kwenye programu yako. Andiko zako bado zinaweza kuonekana kwa umma, pamoja na watu wanaokuzuia. Watumiaji waliozuiliwa wameorodheshwa kwenye skrini ya Filta za Usalama. + + Ripoti Matusi + Ripoti zote zilizopostiwa zitaonekana kwa umma. + Toa muktadha wa ziada ikiwa unataka… + Muktadha wa Ziada + Sababu + Chagua sababu… + Chapisha Ripoti + Zuia na Ripoti + Zuia + Vialamisho + Vialamisho Binafsi + Vialamisho vya Umma + Ongeza kwenye Vialamisho Binafsi + Ongeza kwenye Vialamisho vya Umma + Ondoa kutoka kwenye Vialamisho Binafsi + Ondoa kutoka kwenye Vialamisho vya Umma + Huduma ya Kuchanganya Pochi + Inaidhinisha Nostr Secret kulipa zaps bila kuacha programu. Weka siri salama na tumia relay ya faragha ikiwezekana + Ufunguo wa Umma wa Kuchanganya Pochi + Kuchanganya Relay + Siri ya Kuchanganya Pochi + Onyesha ufunguo wa siri + nsec / ufunguo wa siri wa hex + Kiasi cha Ahadi katika Sats + Chapisha Kura + Sehemu zinazohitajika: + Wapokeaji wa Zaps + Maelezo ya msingi ya kura… + Chaguo %s + Maelezo ya chaguo cha kura + Sehemu za hiari: + Kima cha Chini cha Zaps + Kima cha Juu cha Zaps + Uamuzi + (0–100)% + Funga baada ya + siku + Kura imefungwa kwa kura mpya + Kiasi cha Zaps + Kura moja tu kwa mtumiaji inaruhusiwa kwenye aina hii ya kura + "Inatafuta Tukio %1$s" + Ongeza ujumbe wa umma + Ongeza ujumbe wa faragha + Ongeza ujumbe wa ankra + Asante kwa kazi yako yote! + Tengeneza na Ongeza + Waandishi wa kura hawawezi kupiga kura kwenye kura zao wenyewe. + Yaliyomo haya ni sawa tangu chapisho + Yaliyomo haya yamebadilika. Mwandishi huenda hakuyapata au kuyaidhinisha mabadiliko + Ongeza Picha + Ongeza Video + Ongeza Hati + Ongeza kwenye Ujumbe + Maelezo ya yaliyomo + Boti ya buluu kwenye ufukwe mweupe wa mchanga wakati wa jua + Aina ya Zap + Aina ya Zap kwa chaguo zote + Umma + Kila mtu anaweza kuona shughuli na ujumbe + Faragha + Mpokeaji na Mjumbe wanaweza kuona kila mmoja na kusoma ujumbe + Bila Kutambuliwa + Mpokeaji na umma hawajui ni nani aliyetuma malipo + Bila Zap + Hakuna alama katika Nostr, tu katika Lightning + Seva ya Faili + LnAddress au @Mtumiaji + imgur.com - inaaminika + Imgur inaweza kubadilisha faili + nostrimg.com - inaaminika + NostrImg inaweza kubadilisha faili + nostr.build - inaaminika + Nostr.build inaweza kubadilisha faili + nostrfiles.dev - inaaminika + Nostrfiles.dev inaweza kubadilisha faili + nostrcheck.me - inaaminika + nostrcheck.me inaweza kubadilisha faili + Imgur inayoweza kuthibitika (NIP-94) + Inachunguza ikiwa Imgur imebadilisha faili. NIP mpya: wateja wengine huenda wasiione + NostrImg inayoweza kuthibitika (NIP-94) + Inachunguza ikiwa NostrImg imebadilisha faili. NIP mpya: wateja wengine huenda wasiione + Nostr.build inayoweza kuthibitika (NIP-94) + Inachunguza ikiwa Nostr.build imebadilisha faili. NIP mpya: wateja wengine huenda wasiione + Nostrfiles.dev inayoweza kuthibitika (NIP-94) + Inachunguza ikiwa Nostrfiles.dev imebadilisha faili. NIP mpya: wateja wengine huenda wasiione + Nostrcheck.me inayoweza kuthibitika (NIP-94) + Inachunguza ikiwa Nostrcheck.me imebadilisha faili. NIP mpya: wateja wengine huenda wasiione + Relays Zako (NIP-95) + Faili zinahifadhiwa na relays zako. NIP mpya: hakikisha zinazisaidia + Usanidi wa Tor/Orbot + Unaweza kuunganisha kupitia usanidi wako wa Orbot + Kujitenga na Orbot/Tor yako? + Data yako itahamishwa mara moja kwenye mtandao wa kawaida + Ndio + Hapana + Orodha ya Kufuatilia + Yafuatayo Yote + Kote + ## Kuungana kupitia Tor na Orbot +\n\n1. Sakinisha [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) +\n2. Anza Orbot +\n3. Katika Orbot, angalia bandari ya Socks. Chaguo-msingi hutumia 9050 +\n4. Ikiwa ni lazima, badilisha bandari katika Orbot +\n5. Sanidi bandari ya Socks kwenye skrini hii +\n6. Bonyeza kitufe cha Kuamilisha kutumia Orbot kama mbadala + Bandari ya Socks ya Orbot + Nambari ya bandari sio sahihi + Tumia Orbot + Jitenge na Tor/Orbot yako + Ujumbe Binafsi + Inakuarifu wakati ujumbe binafsi unapowasili + Zaps Zilizopokelewa + Inakuarifu wakati mtu anapokuzap + %1$s sats + Kutoka kwa %1$s + kwa %1$s + Taarifa: + jiunge kwenye mazungumzo + Kitambulisho cha Mtumiaji au Kikundi + npub, nevent au hex + Unda + jiunge + Leo + Onyo la Yaliyomo + Chapisho hili lina yaliyomo nyeti ambayo baadhi ya watu wanaweza kuona kama ya kuchukiza au kusumbua + Daima ficha yaliyomo nyeti + Daima onyesha yaliyomo nyeti + Daima onyesha onyo za yaliyomo + Inapendekeza: + Chuja barua taka kutoka kwa wageni + Onya wakati machapisho yana ripoti kutoka kwa wafuatavyo wako + Alama Mpya ya Majibu + Hakuna aina za majibu zilizochaguliwa. Bonyeza kwa muda mrefu kubadilisha + Mzushi wa Zaps + Inaongeza kiasi lengwa cha sats kwa chapisho hili. Wateja wanaosaidia wanaweza kuonyesha hii kama bar ya maendeleo ili kuhimiza michango + Kiasi Lengo katika Sats + Mzushi wa Zaps kwa %1$s. Sats %2$s hadi kufikia lengo + Soma kutoka kwa Relay + Andika kwa Relay + Kuna kosa lililotokea jaribu kupata habari za Relay kutoka kwa %1$s + Mmiliki + Toleo + Programu + Wasiliana + NIPs Zinazoungwa mkono + Ada za Kuingia + URL ya Malipo + Vikwazo + Nchi + Lugha + Alama + Sera ya Kutuma + Urefu wa Ujumbe + Michango + Vichujio + Urefu wa Kitambulisho cha Michango + Awamu ya Chini + Vichwa vya Tukio vya Juu + Urefu wa Yaliyomo + PoW ya Chini + Uthibitishaji + Malipo + Cashu Token + Kukomboa + Hakuna Anwani ya Lightning Imewekwa + Kitufe kimekopwa kwenye ubao wa kunakili + Moja kwa Moja + Nje ya Mtandao + Imemalizika + Imepangwa + Moja kwa Moja Imekwama + Moja kwa Moja Imekwisha + Kutoka kunaondoa taarifa zako za eneo la kuhifadhia data. Hakikisha una nakala za funguo zako binafsi ili kuepuka kupoteza akaunti yako. Je, unataka kuendelea? + Mada Zinazofuatwa + Usanidi wa Relays + Moja kwa Moja + Jumuiya + Mazungumzo + Machapisho Yaliyoidhinishwa + Kikundi hiki hakitumii maelezo au kanuni. Ongea na mmiliki ili aongeze. + Jumuiya hii haitumii maelezo. Ongea na mmiliki ili aongeze. + Yaliyomo Nyeti + Inaongeza onyo la yaliyomo nyeti kabla ya kuonyesha yaliyomo hii + Mipangilio + Daima + Wi-Fi tu + Kamwe + Mfumo + Mwanga + Giza + Mapendeleo ya Programu + Lugha + Mandhari + Onesha Picha + Cheza Video + Onesha Hakikisho za URL + Soma Picha + Wahasibu + Walemavu. Bonyeza kuwasha sauti + Sauti imewashwa. Bonyeza kulegeza + Tafuta rekodi za eneo la kuhifadhia data na rekodi za mbali + Anwani ya Nostr ilithibitishwa + Anwani ya Nostr ilishindwa kuthibitishwa + Kuthibitisha Anwani ya Nostr + Chagua/Chagua Vingine Vyote + Chaguo-msingi + Chagua kiotomatiki kuendelea + Tuma Zaps kwa: + Wateja wanaosaidia watatuma Zaps kwa LNAddress au Wasifu wa Mtumiaji hapa chini badala ya yako + Tumia Mahali kama + Inaongeza Geohash ya eneo lako kwenye chapisho. Umma utajua kuwa uko ndani ya kilometa 5 (maili 3) ya eneo la sasa + Inaongeza onyo la yaliyomo nyeti kabla ya kuonyesha yaliyomo yako. Hii ni nzuri kwa yaliyomo yoyote ya NSFW au yaliyomo ambayo baadhi ya watu wanaweza kuona kama ya kuchukiza au kusumbua + Kipengee Kipya + Kuwezesha hali hii kunahitaji Amethyst kutuma ujumbe wa NIP-24 (Ufungaji wa Zawadi, Ujumbe wa Moja kwa Moja Uliosajiliwa, na Ujumbe wa Kikundi). NIP-24 ni mpya na wateja wengi bado hawajaijumuisha. Hakikisha mpokeaji anatumia mteja unaokubaliana. + Tumia + Umma + Binafsi + Kwa + Somo + Mada ya mazungumzo + "\@User1, @User2, @User3" + Wanachama wa kikundi hiki + Maelezo kwa wanachama + Kubadilisha jina kwa malengo mapya. + Kwa Interface ya Programu + Mada ya Giza, Mada ya Mwanga, au Mada ya Mfumo + Lisha picha na GIFs kiotomatiki + Cheza video na GIFs kiotomatiki + Onyesha hakikisho za URL kiotomatiki + Lini kusoma picha + Nakili URL kwa ubao wa kunakili + Nakili Kitambulisho cha Andiko kwa ubao wa kunakili + Imetengenezwa saa + Miongozo + Sasisha hali yako + Kosa katika uchambuzi wa ujumbe wa kosa + Kura zina uzito kulingana na kiasi cha zap. Unaweza kuweka kiasi cha chini ili kuepuka wahasibu na kiasi cha juu ili kuepuka wahasibu wakubwa kuchukua kura ya maoni. Tumia kiasi sawa katika sehemu zote mbili ili kuhakikisha kila kura inathaminiwa kwa kiasi sawa. Acha tupu ili kukubali kiasi chochote. + Haiwezekani kutuma zap + Tuma ujumbe kwa mtumiaji + Sawa + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4d9ec0a96..bd6a9a69b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -123,6 +123,7 @@ 发送直接消息 编辑用户元数据 关注 + 回关 不隐藏 复制用户ID 隐藏该用户 @@ -251,6 +252,9 @@ 显示私钥 nsec / 十六进制私钥 聪数量 + 必填字段: + 打闪接收人 + 选项 %s 可选字段: 打闪最低金额 打闪最高金额 @@ -259,6 +263,7 @@ 打闪金额 "“正在查找事件%1$s”" 添加公开消息 + 添加私信 添加发票消息 创建并添加 添加图片 @@ -268,18 +273,50 @@ 内容描述 一艘蓝色船,位于白沙滩的日落点 打闪种类 + 所有选项的打闪类型 公开 所有人都能看到交易和消息 + 私人 发送方和接收方能互相看到并读取消息 匿名 非打闪 + 你的中继器 (NIP-95) + Tor/Orbot 设置 + 通过你的 Orbot 设置连接 断开与你的 Orbot/Tor 连接? + 关注列表 + 所有关注 全球 + Orbot Socks 端口 + 无效端口 使用 Orbot + 断开 Tor/Orbot 连接 + 私信 + 收到私信时通知你 + 收到打闪 + 收到打闪时通知你 + %1$s聪 + 通知: + 加入对话 + 用户或群组 ID + npub、nevent 或 hex + 创建 + 加入 今天 + 内容警告 + 始终隐藏敏感内容 + 始终显示敏感内容 + 始终显示内容警告 推荐: + 过滤来自陌生人的垃圾信息 + 新反应符号 + Zapraiser + 目标聪金额 + Zapraiser 位于 %1$s。距离目标%2$s聪 + 从中继器读取 + 写入到继电器 版本 软件 联络 @@ -293,13 +330,24 @@ 消息长度 订阅 筛选器 + 订阅 ID 长度 + 内容长度 + 认证 + 支付 + 兑换 + 未设置闪电地址 + 已复制令牌至剪贴板 + 直播 离线 已结束 + 直播处于离线状态 + 直播已结束 已关注的标签 中继器 直播 社区 聊天 + 批准帖子 敏感内容 设置 始终 @@ -308,13 +356,45 @@ 系统 浅色 深色 + 应用程序首选项 语言 主题 + 图像预览 + 视频播放 + URL 预览 加载图像 + 静音。点击取消静音 + 声音开启。点击静音 Nostr 地址已验证 Nostr 地址验证失败 正在检查 Nostr 地址 默认 + 选择中继器以继续 + 将打闪转发到: + 将位置显示为 + 新功能 + 启用 公开 + 私人 + + 主题 + 对话主题 + "\@User1、@User2、@User3" + 此群组成员 + 对成员的解释 + 用于应用程序界面 + 暗色、亮色或系统主题 + 自动加载图像和 GIF + 自动播放视频和 GIF + 显示 URL 预览 + 何时加载图像 + 复制链接到剪贴板 + 复制笔记 ID 到剪贴板 创建于 + 规则 + 更新你的状态 + 错误解析错误消息 + 无法发送打闪 + 向用户发送消息 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index eaa2a1c33..6fdb0b002 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -233,8 +233,10 @@ 添加視頻 添加文件 添加到消息 + 內容描述 打閃種類 公開 + 私人 匿名 非打閃 Tor/Orbot 設置 @@ -242,6 +244,34 @@ 斷開與你的 Orbot/Tor 連接? + 關注列表 + 所有關注 + 全球 + Orbot Socks 端口 + 無效端口 + 使用 Orbot + 斷連 Tor/Orbot + 私信 + %1$s聰 + 來自%1$s + npub,、nevent 或 hex + 創建 + 加入 + 今天 + 內容警告 + 推薦: + 版本 + 軟體 + 聯絡 + 限制 + 國家 + 語言 + 標籤 + 訂閱 + 認證 + 付款 + 兌換 + 未設定閃電地址 直播 離線 已結束 @@ -250,6 +280,8 @@ 中繼器 直播 社群 + 聊天 + 敏感內容 設置 始終 永不 @@ -261,5 +293,23 @@ 圖像預覽 鏈接預覽 加載圖像 + Nostr 地址已驗證 + Nostr 地址驗證失敗 + 正在檢查 Nostr 地址 默認 + 將打閃轉發到: + 新功能 + 啟用 + 公開 + 私人 + + 主題 + 何時加載圖像 + 將 URL 複製到剪貼簿 + 將筆記 ID 複製到剪貼簿 + 創建於 + 規則 + 更新你的狀態 + 無法發送打閃 + 確定 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 200383078..1a7d050aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -562,4 +562,16 @@ Unable to send zap Message the User Ok + + Failed to reach %1$s: %2$s + Failed to reach %1$s: %2$s + Failed to parse result from %1$s: %2$s + %1$s failed with code %2$s + Active for: + + Home + DMs + Chats + Global + Search diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index b1ffdebbd..22d8cc370 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -10,6 +10,7 @@ + diff --git a/crowdin.yml b/crowdin.yml index 22413f7a3..c822c312a 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -19,6 +19,7 @@ files: fa: fa fr: fr hu: hu + in: in ja: ja nl: nl ru: ru diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt index bedf72d65..c93209059 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt @@ -4,7 +4,7 @@ import java.util.regex.Pattern object LnWithdrawalUtil { private val withdrawalPattern = Pattern.compile( - "lnurl.+", + "lnurl1[02-9ac-hj-np-z]+", Pattern.CASE_INSENSITIVE ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 5aa91ebf3..ae78d7bc4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -62,7 +62,7 @@ open class Event( override fun toJson(): String = mapper.writeValueAsString(toJsonObject()) - fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" } + override fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" } override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index 61ba9dbc6..64d308777 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -63,6 +63,8 @@ interface EventInterface { fun subject(): String? fun zapraiserAmount(): Long? + fun hasAnyTaggedUser(): Boolean + fun taggedAddresses(): List fun taggedUsers(): List fun taggedEvents(): List