Compare commits

..
39 Commits
Author SHA1 Message Date
Vitor Pamplona 29b8f6916b v0.87.4 2024-06-03 17:29:43 -04:00
Vitor Pamplona a6519d57c6 Passes the video note uri so that when the user clicks in the video popup the app brings it back to the playing screen without breaking the video. 2024-06-03 17:26:40 -04:00
Vitor Pamplona 2b2ee5bbfc Avoids using Video Cache for live streams. 2024-06-03 17:18:38 -04:00
Vitor Pamplona 62a6b7b7e0 Fixes video stream position for live streams. 2024-06-03 17:18:17 -04:00
Vitor Pamplona 5e0a1f9de1 Moves SimpleCache creation to the IO thread. 2024-06-03 17:17:26 -04:00
Vitor PamplonaandGitHub 571b1bc9cf Merge pull request #900 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 14:27:47 -04:00
Crowdin Bot a3e76225e6 New Crowdin translations by GitHub Action 2024-06-03 18:26:14 +00:00
Vitor Pamplona 4388ceb529 v0.87.3 2024-06-03 14:24:28 -04:00
Vitor Pamplona 5d4fa0d732 Adds the incoming relays to the request. 2024-06-03 14:15:12 -04:00
Vitor Pamplona ed6825de97 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 14:04:46 -04:00
Vitor Pamplona 10f623b6b3 Forces URLs to be single line. 2024-06-03 14:04:38 -04:00
Vitor Pamplona 467e5c0e4a Fixes bug that every socket is null 2024-06-03 14:04:29 -04:00
Vitor Pamplona 09d2e9593e Adds error messages to relay information dialog 2024-06-03 14:03:58 -04:00
Vitor PamplonaandGitHub cd20c0567b Merge pull request #899 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 13:27:05 -04:00
Vitor Pamplona db15f5e2a6 Uses the user to avoid Java removing the variable due to lack of usage. 2024-06-03 13:25:56 -04:00
Crowdin Bot 18d5c7e3f4 New Crowdin translations by GitHub Action 2024-06-03 16:08:05 +00:00
Vitor Pamplona d42ae7c9f7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 12:06:38 -04:00
Vitor PamplonaandGitHub 2b741b62e8 Merge pull request #898 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 12:06:14 -04:00
Vitor Pamplona 4e1c63d36a allows more replies to filter when using DVMs 2024-06-03 12:05:58 -04:00
Vitor Pamplona 95797171d7 Fixes observer of authors 2024-06-03 12:05:46 -04:00
Crowdin Bot 395ef9695e New Crowdin translations by GitHub Action 2024-06-03 15:49:13 +00:00
Vitor Pamplona ad1c7088c7 Loads the DVM's NIP65 records. 2024-06-03 11:47:39 -04:00
Vitor Pamplona eda4569b64 Only shows debug relay writes in DEBUG 2024-06-03 11:47:20 -04:00
Vitor Pamplona fb5f1e738b Adds a user and max result params to the DVM request. 2024-06-03 11:47:00 -04:00
Vitor Pamplona e3cca6c691 Pulls NIP-65 relays even in the first lookup for the user. 2024-06-03 11:45:57 -04:00
Vitor Pamplona ce2b8dfa11 Sends the DVM request to the NIP65 Inbox relays. 2024-06-03 11:15:36 -04:00
Vitor Pamplona b5faf6ffc5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 11:15:07 -04:00
Vitor PamplonaandGitHub 4ee157f27f Merge pull request #897 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 11:14:50 -04:00
Vitor Pamplona ad8c04b79a Loads user's relays on scrolling. 2024-06-03 11:14:33 -04:00
Crowdin Bot c0e6273954 New Crowdin translations by GitHub Action 2024-06-03 15:04:13 +00:00
Vitor Pamplona aebade21a6 Don't display zap splits when the post is short. 2024-06-03 11:02:23 -04:00
Vitor Pamplona bda24c0101 Separates the Account's relay info to the UI component 2024-06-03 11:01:34 -04:00
Vitor Pamplona 3d0b461550 Moves Relay Stats to it's own cache.
Refactors the isLocalRelay function
Removes unnecessary onError methods from the Relay class
Moves byte counts from Int to Long
2024-06-03 10:42:00 -04:00
Vitor PamplonaandGitHub 5900e6c49f Merge pull request #896 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 08:45:33 -04:00
Crowdin Bot f29fe3ced5 New Crowdin translations by GitHub Action 2024-06-03 12:38:46 +00:00
Vitor Pamplona bf17cc8f03 Updating Firebase and Androidx JUnit 2024-06-03 08:36:45 -04:00
Vitor Pamplona a8d10c22d4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-01 16:14:47 -04:00
Vitor PamplonaandGitHub 5492ebf43f Merge pull request #893 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-01 16:13:05 -04:00
Crowdin Bot d66a027270 New Crowdin translations by GitHub Action 2024-06-01 19:33:36 +00:00
66 changed files with 991 additions and 624 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 371
versionName "0.87.2"
versionCode 373
versionName "0.87.4"
buildConfigField "String", "RELEASE_NOTES_ID", "\"863124b6592359494396c22b917f9d251ad12ac31b09ea29e0265e3c088ce9f8\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -36,6 +36,7 @@ fun TranslatableRichTextViewer(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) = ExpandableRichTextViewer(
@@ -46,6 +47,7 @@ fun TranslatableRichTextViewer(
tags,
backgroundColor,
id,
callbackUri,
accountViewModel,
nav,
)
@@ -40,6 +40,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import kotlin.time.measureTimedValue
@@ -53,7 +54,9 @@ class Amethyst : Application() {
val videoCache: VideoCache by lazy {
val newCache = VideoCache()
newCache.initFileCache(this)
runBlocking {
newCache.initFileCache(this@Amethyst)
}
newCache
}
@@ -781,10 +781,7 @@ class Account(
note.event?.let { event ->
LnZapRequestEvent.create(
event,
relays =
getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(),
relays = getReceivingRelays(),
signer,
pollOption,
message,
@@ -795,6 +792,12 @@ class Account(
}
}
fun getReceivingRelays(): Set<String> {
return getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile().latestContactList?.relays()?.filter { it.value.read }?.keys?.ifEmpty { null }
?: localRelays.filter { it.read }.map { it.url }.toSet()
}
fun hasWalletConnectSetup(): Boolean {
return zapPaymentRequest != null
}
@@ -2462,8 +2465,14 @@ class Account(
dvmPublicKey: String,
onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit,
) {
NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey, signer) {
Client.send(it)
NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey, signer.pubKey, getReceivingRelays(), signer) {
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(dvmPublicKey))?.event as? AdvertisedRelayListEvent)?.readRelays()
if (relayList != null) {
Client.sendPrivately(it, relayList)
} else {
Client.send(it)
}
LocalCache.justConsume(it, null)
onReady(it)
}
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
@@ -69,10 +70,6 @@ class AntiSpamFilter {
if (
(recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null
) {
Log.w(
"Potential SPAM Message for sharing",
"${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, null)}",
)
Log.w(
"Potential SPAM Message",
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}",
@@ -81,6 +78,10 @@ class AntiSpamFilter {
// Log down offenders
logOffender(hash, event)
if (relay != null) {
RelayStats.newSpam(relay.url, "https://njump.me/${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay.url)}")
}
liveSpam.invalidateData()
return true
@@ -100,7 +101,7 @@ class AntiSpamFilter {
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
} else {
val spammer = spamMessages.get(hashCode)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
spammer.duplicatedMessages += event.id
}
}
@@ -450,7 +450,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -474,6 +473,8 @@ object LocalCache {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Log.d("TN", "New Response ${event.taggedEvents().joinToString(", ") { it }}}")
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
@@ -623,7 +624,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -648,7 +648,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -673,7 +672,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -711,7 +709,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -746,7 +743,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -827,7 +823,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -1533,7 +1528,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -1572,7 +1566,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -28,12 +28,5 @@ data class RelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val errorCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val feedTypes: Set<FeedType>,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
)
@@ -31,11 +31,12 @@ object CachedRichTextParser {
fun parseText(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String? = null,
): RichTextViewerState {
return if (richTextCache[content] != null) {
richTextCache[content]
} else {
val newUrls = RichTextParser().parseText(content, tags)
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
richTextCache.put(content, newUrls)
newUrls
}
@@ -103,6 +103,16 @@ object HttpClientManager {
}
}
fun getHttpClientForUrl(url: String): OkHttpClient {
// TODO: How to identify relays on the local network?
val isLocalHost = url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")
return if (isLocalHost) {
getHttpClient(false)
} else {
getHttpClient()
}
}
fun getHttpClient(useProxy: Boolean = true): OkHttpClient {
return if (useProxy) {
if (this.defaultHttpClient == null) {
@@ -112,9 +112,8 @@ class Nip11Retriever {
try {
val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
val isLocalHost = dirtyUrl.startsWith("ws://127.0.0.1") || dirtyUrl.startsWith("ws://localhost")
HttpClientManager.getHttpClient(useProxy = !isLocalHost)
HttpClientManager.getHttpClientForUrl(dirtyUrl)
.newCall(request)
.enqueue(
object : Callback {
@@ -92,19 +92,6 @@ abstract class NostrDataSource(val debugName: String) {
}
}
override fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) {
// if (subscriptions.containsKey(subscriptionId)) {
// Log.e(
// this@NostrDataSource.javaClass.simpleName,
// "Relay OnError ${relay.url}: ${error.message}"
// )
// }
}
override fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
@@ -178,8 +178,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
limit = 100,
),
),
)
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.StatusEvent
@@ -44,7 +45,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = firstTimers,
),
),
@@ -67,7 +68,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND),
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = groupIds,
since = minEOSEs,
),
@@ -86,6 +86,8 @@ class MultiPlayerPlaybackManager(
applicationContext: Context,
): MediaSession {
val existingSession = playingMap.get(id) ?: cache.get(id)
// avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true)
if (existingSession != null) return existingSession
val player =
@@ -115,7 +117,9 @@ class MultiPlayerPlaybackManager(
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
cachedPositions.add(uri, player.currentPosition)
if (mustCachePositions) {
cachedPositions.add(uri, player.currentPosition)
}
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
@@ -125,20 +129,22 @@ class MultiPlayerPlaybackManager(
when (playbackState) {
STATE_IDLE -> {
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
if (mustCachePositions) {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
}
}
}
}
else -> {
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
@@ -150,7 +156,9 @@ class MultiPlayerPlaybackManager(
newPosition: PositionInfo,
reason: Int,
) {
cachedPositions.add(uri, newPosition.positionMs)
if (mustCachePositions) {
cachedPositions.add(uri, newPosition.positionMs)
}
}
},
)
@@ -34,41 +34,38 @@ import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.HttpClientManager
import okhttp3.OkHttpClient
class WssOrHttpFactory(httpClient: OkHttpClient) : MediaSource.Factory {
@UnstableApi
val http = DefaultMediaSourceFactory(OkHttpDataSource.Factory(httpClient))
/**
* HLS LiveStreams cannot use cache.
*/
@UnstableApi
class CustomMediaSourceFactory() : MediaSource.Factory {
private var cachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
private var nonCachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
@UnstableApi
val wss = DefaultMediaSourceFactory(WssStreamDataSource.Factory(httpClient))
@OptIn(UnstableApi::class)
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
http.setDrmSessionManagerProvider(drmSessionManagerProvider)
wss.setDrmSessionManagerProvider(drmSessionManagerProvider)
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
return this
}
@OptIn(UnstableApi::class)
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
http.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
wss.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
return this
}
@OptIn(UnstableApi::class)
override fun getSupportedTypes(): IntArray {
return http.supportedTypes
return nonCachingFactory.supportedTypes
}
@OptIn(UnstableApi::class)
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
return if (mediaItem.mediaId.startsWith("wss")) {
wss.createMediaSource(mediaItem)
} else {
http.createMediaSource(mediaItem)
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}
@@ -81,7 +78,7 @@ class PlaybackService : MediaSessionService() {
fun newAllInOneDataSource(): MediaSource.Factory {
// This might be needed for live kit.
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
return DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
return CustomMediaSourceFactory()
}
fun lazyDS(): MultiPlayerPlaybackManager {
@@ -27,6 +27,8 @@ import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.io.File
@@ -41,16 +43,17 @@ class VideoCache {
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
@Synchronized
fun initFileCache(context: Context) {
suspend fun initFileCache(context: Context) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
simpleCache =
SimpleCache(
File(context.cacheDir, "exoplayer"),
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
withContext(Dispatchers.IO) {
simpleCache =
SimpleCache(
File(context.cacheDir, "exoplayer"),
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
}
}
// This method should be called when proxy setting changes.
@@ -194,19 +194,6 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) {
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch(Dispatchers.Default) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onRelayStateChange(
type: Relay.StateType,
@@ -284,13 +271,6 @@ object Client : RelayPool.Listener {
afterEOSE: Boolean,
) = Unit
/** A new or repeat message was received */
open fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) = Unit
/** Connected to or disconnected from a relay */
open fun onRelayStateChange(
type: Relay.StateType,
@@ -59,38 +59,24 @@ class Relay(
val write: Boolean = true,
val activeTypes: Set<FeedType> = FeedType.values().toSet(),
) {
val brief = RelayBriefInfoCache.get(url)
companion object {
// waits 3 minutes to reconnect once things fail
const val RECONNECTING_IN_SECONDS = 60 * 3
}
private val httpClient =
if (url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")) {
HttpClientManager.getHttpClient(false)
} else {
HttpClientManager.getHttpClient()
}
val brief = RelayBriefInfoCache.get(url)
private var listeners = setOf<Listener>()
private var socket: WebSocket? = null
private var isReady: Boolean = false
private var usingCompression: Boolean = false
var eventDownloadCounterInBytes = 0
var eventUploadCounterInBytes = 0
private var lastConnectTentative: Long = 0L
var spamCounter = 0
var errorCounter = 0
var pingInMs: Long? = null
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
var lastConnectTentative: Long = 0L
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
val authResponse = mutableMapOf<HexKey, Boolean>()
val sendWhenReady = mutableListOf<EventInterface>()
private val authResponse = mutableMapOf<HexKey, Boolean>()
private val sendWhenReady = mutableListOf<EventInterface>()
fun register(listener: Listener) {
listeners = listeners.plus(listener)
@@ -116,7 +102,7 @@ class Relay(
private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) {
Log.d("Relay", "Relay.connect $url connecting: ${connectingBlock.get()} hasProxy: ${this.httpClient.proxy != null}")
Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingBlock.get()}")
// BRB is crashing OkHttp Deflater object :(
if (url.contains("brb.io")) return
@@ -141,13 +127,13 @@ class Relay(
.url(url.trim())
.build()
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
socket = HttpClientManager.getHttpClientForUrl(url).newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) {
if (e is CancellationException) throw e
errorCounter++
RelayStats.newError(url, e.message)
markConnectionAsClosed()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
} finally {
connectingBlock.set(false)
@@ -187,7 +173,7 @@ class Relay(
) {
checkNotInMainThread()
eventDownloadCounterInBytes += text.bytesUsedInMemory()
RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
try {
processNewRelayMessage(text)
@@ -239,21 +225,25 @@ class Relay(
) {
checkNotInMainThread()
errorCounter++
socket?.cancel() // 1000, "Normal close"
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
RelayStats.newError(url, response?.message ?: t.message)
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
t.printStackTrace()
listeners.forEach {
it.onError(
this@Relay,
"",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
)
}
}
// Failures disconnect the relay.
markConnectionAsClosed()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
t.printStackTrace()
listeners.forEach {
it.onError(
this@Relay,
"",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
)
}
}
}
@@ -263,8 +253,9 @@ class Relay(
) {
this.resetEOSEStatuses()
this.isReady = true
this.pingInMs = pingInMs
this.usingCompression = usingCompression
RelayStats.setPing(url, pingInMs)
}
fun markConnectionAsClosed() {
@@ -306,6 +297,8 @@ class Relay(
val message = msgArray.get(1).asText()
Log.w("Relay", "Relay onNotice $url, $message")
RelayStats.newNotice(url, message)
it.onError(this@Relay, message, Error("Relay sent notice: $message"))
}
"OK" ->
@@ -336,7 +329,9 @@ class Relay(
it.onNotify(this@Relay, msgArray[1].asText())
}
"CLOSED" -> listeners.forEach { Log.w("Relay", "Relay onClosed $url, $newMessage") }
else ->
else -> {
RelayStats.newError(url, "Unsupported message: $newMessage")
listeners.forEach {
Log.w("Relay", "Unsupported message: $newMessage")
it.onError(
@@ -345,6 +340,7 @@ class Relay(
Error("Unknown type $type on channel. Msg was $newMessage"),
)
}
}
}
}
@@ -389,10 +385,8 @@ class Relay(
it.filter.toJson(url)
}
// Log.d("Relay", "onFilterSent $url $requestId $request")
writeToSocket(request)
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
resetEOSEStatuses()
}
}
@@ -457,30 +451,9 @@ class Relay(
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
sendAuth(signedEvent)
} else {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) {
if (isReady) {
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
} else {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything.
renewFilters()
}
}
sendEvent(signedEvent)
}
}
@@ -488,18 +461,12 @@ class Relay(
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
sendAuth(signedEvent)
} else {
if (write) {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) {
if (isReady) {
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
} else {
synchronized(sendWhenReady) {
sendWhenReady.add(signedEvent)
@@ -508,10 +475,7 @@ class Relay(
} else {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
// Sends everything.
renewFilters()
@@ -521,12 +485,42 @@ class Relay(
}
}
fun close(subscriptionId: String) {
checkNotInMainThread()
private fun sendAuth(signedEvent: RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
}
val msg = """["CLOSE","$subscriptionId"]"""
// Log.d("Relay", "Close Subscription $url $msg")
socket?.send(msg)
private fun sendEvent(signedEvent: EventInterface) {
if (isConnected()) {
if (isReady) {
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
}
} else {
// sends all filters after connection is successful.
connectAndRun {
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
// Sends everything.
renewFilters()
}
}
}
private fun writeToSocket(str: String) {
socket?.let {
checkNotInMainThread()
it.send(str)
RelayStats.addBytesSent(url, str.bytesUsedInMemory())
if (BuildConfig.DEBUG) {
Log.d("Relay", "Relay send $url $str")
}
}
}
fun close(subscriptionId: String) {
writeToSocket("""["CLOSE","$subscriptionId"]""")
}
fun isSameRelayConfig(other: Relay): Boolean {
@@ -197,12 +197,6 @@ object RelayPool : Relay.Listener {
afterEOSE: Boolean,
)
fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
)
fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
@@ -241,7 +235,6 @@ object RelayPool : Relay.Listener {
subscriptionId: String,
error: Error,
) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
updateStatus()
}
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.relays
import androidx.collection.LruCache
import com.vitorpamplona.quartz.utils.TimeUtils
object RelayStats {
private val innerCache = mutableMapOf<String, RelayStat>()
fun get(url: String): RelayStat {
return innerCache.getOrPut(url) { RelayStat() }
}
fun addBytesReceived(
url: String,
bytesUsedInMemory: Int,
) {
get(url).addBytesReceived(bytesUsedInMemory)
}
fun addBytesSent(
url: String,
bytesUsedInMemory: Int,
) {
get(url).addBytesSent(bytesUsedInMemory)
}
fun newError(
url: String,
error: String?,
) {
get(url).newError(error)
}
fun newNotice(
url: String,
notice: String?,
) {
get(url).newNotice(notice)
}
fun setPing(
url: String,
pingInMs: Long,
) {
get(url).pingInMs = pingInMs
}
fun newSpam(
url: String,
explanation: String,
) {
get(url).newSpam(explanation)
}
}
class RelayStat(
var receivedBytes: Long = 0L,
var sentBytes: Long = 0L,
var spamCounter: Long = 0L,
var errorCounter: Long = 0L,
var pingInMs: Long = 0L,
) {
val messages = LruCache<RelayDebugMessage, RelayDebugMessage>(100)
fun newNotice(notice: String?) {
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.NOTICE,
message = notice ?: "No error message provided",
)
messages.put(debugMessage, debugMessage)
}
fun newError(error: String?) {
errorCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.ERROR,
message = error ?: "No error message provided",
)
messages.put(debugMessage, debugMessage)
}
fun addBytesReceived(bytesUsedInMemory: Int) {
receivedBytes += bytesUsedInMemory
}
fun addBytesSent(bytesUsedInMemory: Int) {
sentBytes += bytesUsedInMemory
}
fun newSpam(spamDescriptor: String) {
spamCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.SPAM,
message = spamDescriptor,
)
messages.put(debugMessage, debugMessage)
}
}
class RelayDebugMessage(
val type: RelayDebugMessageType,
val message: String,
val time: Long = TimeUtils.now(),
)
enum class RelayDebugMessageType {
SPAM,
NOTICE,
ERROR,
}
@@ -308,7 +308,7 @@ fun EditPostView(
accountViewModel = accountViewModel,
)
} else {
LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -323,7 +323,7 @@ fun EditPostView(
nav = nav,
)
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
}
}
}
@@ -429,7 +429,7 @@ fun NewPostView(
accountViewModel = accountViewModel,
)
} else {
LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -444,7 +444,7 @@ fun NewPostView(
nav = nav,
)
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
}
}
}
@@ -63,15 +63,16 @@ fun NotifyRequestDialog(
val background = remember { mutableStateOf(defaultBackground) }
TranslatableRichTextViewer(
textContent,
content = textContent,
canPreview = true,
quotesLeft = 1,
Modifier.fillMaxWidth(),
EmptyTagList,
background,
textContent,
accountViewModel,
nav,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = background,
id = textContent,
callbackUri = null,
accountViewModel = accountViewModel,
nav = nav,
)
},
confirmButton = {
@@ -48,6 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -236,7 +237,7 @@ fun ResetKind3Relays(postViewModel: Kind3RelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
Constants.defaultRelays.forEach { postViewModel.addRelay(it) }
postViewModel.addAll(Constants.defaultRelays)
postViewModel.loadRelayDocuments()
},
) {
@@ -249,7 +250,7 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it)) }
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
@@ -22,14 +22,25 @@ package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayStat
@Immutable
data class BasicRelaySetupInfo(
val url: String,
val errorCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
@Immutable
data class Kind3BasicRelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val feedTypes: Set<FeedType>,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
@@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayStats
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -78,20 +78,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
val relayList = getRelayList() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
}
@@ -28,6 +28,14 @@ fun countToHumanReadableBytes(counter: Int) =
else -> "$counter"
}
fun countToHumanReadableBytes(counter: Long) =
when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB"
counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB"
counter >= 1000 -> "${Math.round(counter / 1000f)} KB"
else -> "$counter"
}
fun countToHumanReadable(
counter: Int,
str: String,
@@ -37,3 +45,13 @@ fun countToHumanReadable(
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
else -> "$counter $str"
}
fun countToHumanReadable(
counter: Long,
str: String,
) = when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str"
counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str"
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
else -> "$counter $str"
}
@@ -68,11 +68,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -95,7 +95,7 @@ import kotlinx.coroutines.launch
@Composable
fun Kind3RelayListView(
feedState: List<RelaySetupInfo>,
feedState: List<Kind3BasicRelaySetupInfo>,
postViewModel: Kind3RelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
@@ -110,7 +110,7 @@ fun Kind3RelayListView(
}
fun LazyListScope.renderKind3Items(
feedState: List<RelaySetupInfo>,
feedState: List<Kind3BasicRelaySetupInfo>,
postViewModel: Kind3RelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
@@ -147,14 +147,17 @@ fun ServerConfigPreview() {
ClickableRelayItem(
loadProfilePicture = true,
item =
RelaySetupInfo(
Kind3BasicRelaySetupInfo(
url = "nostr.mom",
read = true,
write = true,
errorCount = 23,
downloadCountInBytes = 10000,
uploadCountInBytes = 10000000,
spamCount = 10,
relayStat =
RelayStat(
errorCounter = 23,
receivedBytes = 10000,
sentBytes = 10000000,
spamCounter = 10,
),
feedTypes = Constants.activeTypesGlobalChats,
paidRelay = true,
),
@@ -172,15 +175,15 @@ fun ServerConfigPreview() {
@Composable
fun LoadRelayInfo(
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,
item: Kind3BasicRelaySetupInfo,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -263,16 +266,16 @@ fun LoadRelayInfo(
@Composable
fun ClickableRelayItem(
item: RelaySetupInfo,
item: Kind3BasicRelaySetupInfo,
loadProfilePicture: Boolean,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
onToggleFollows: (RelaySetupInfo) -> Unit,
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
onTogglePublicChats: (RelaySetupInfo) -> Unit,
onToggleGlobal: (RelaySetupInfo) -> Unit,
onToggleSearch: (RelaySetupInfo) -> Unit,
onDelete: (RelaySetupInfo) -> Unit,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
onClick: () -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
@@ -333,9 +336,9 @@ fun ClickableRelayItem(
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun StatusRow(
item: RelaySetupInfo,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
item: Kind3BasicRelaySetupInfo,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
modifier: Modifier,
) {
val scope = rememberCoroutineScope()
@@ -370,7 +373,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadableBytes(item.downloadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -406,7 +409,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadableBytes(item.uploadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -432,7 +435,7 @@ private fun StatusRow(
},
),
tint =
if (item.errorCount > 0) {
if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -440,7 +443,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadable(item.errorCount, "errors"),
text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -466,7 +469,7 @@ private fun StatusRow(
},
),
tint =
if (item.spamCount > 0) {
if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -474,7 +477,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadable(item.spamCount, "spam"),
text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -485,11 +488,11 @@ private fun StatusRow(
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun ActiveToggles(
item: RelaySetupInfo,
onToggleFollows: (RelaySetupInfo) -> Unit,
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
onTogglePublicChats: (RelaySetupInfo) -> Unit,
onToggleGlobal: (RelaySetupInfo) -> Unit,
item: Kind3BasicRelaySetupInfo,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
@@ -639,9 +642,9 @@ private fun ActiveToggles(
@Composable
private fun FirstLine(
item: RelaySetupInfo,
item: Kind3BasicRelaySetupInfo,
onClick: () -> Unit,
onDelete: (RelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
modifier: Modifier,
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
@@ -680,7 +683,7 @@ private fun FirstLine(
@Composable
fun Kind3RelayEditBox(
relayToAdd: String,
onNewRelay: (RelaySetupInfo) -> Unit,
onNewRelay: (Kind3BasicRelaySetupInfo) -> Unit,
) {
var url by remember { mutableStateOf<String>(relayToAdd) }
var read by remember { mutableStateOf(true) }
@@ -735,11 +738,12 @@ fun Kind3RelayEditBox(
if (url.isNotBlank() && url != "/") {
val addedWSS = RelayUrlFormatter.normalize(url)
onNewRelay(
RelaySetupInfo(
addedWSS,
read,
write,
feedTypes = FeedType.values().toSet(),
Kind3BasicRelaySetupInfo(
url = addedWSS,
read = read,
write = write,
feedTypes = FeedType.entries.toSet(),
relayStat = RelayStat(),
),
)
url = ""
@@ -27,7 +27,7 @@ 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
import com.vitorpamplona.amethyst.service.relays.RelayStats
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,7 +38,7 @@ import kotlinx.coroutines.launch
class Kind3RelayListViewModel : ViewModel() {
private lateinit var account: Account
private val _relays = MutableStateFlow<List<RelaySetupInfo>>(emptyList())
private val _relays = MutableStateFlow<List<Kind3BasicRelaySetupInfo>>(emptyList())
val relays = _relays.asStateFlow()
var hasModified = false
@@ -52,7 +52,16 @@ class Kind3RelayListViewModel : ViewModel() {
fun create() {
if (hasModified) {
viewModelScope.launch(Dispatchers.IO) {
account.saveKind3RelayList(relays.value)
account.saveKind3RelayList(
relays.value.map {
RelaySetupInfo(
it.url,
it.read,
it.write,
it.feedTypes,
)
},
)
clear()
}
}
@@ -80,7 +89,6 @@ class Kind3RelayListViewModel : ViewModel() {
if (relayFile != null) {
relayFile
.map {
val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes =
account.localRelays
.filter { localRelay -> localRelay.url == it.key }
@@ -92,54 +100,55 @@ class Kind3RelayListViewModel : ViewModel() {
?.feedTypes
?: FeedType.values().toSet().toImmutableSet()
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
RelaySetupInfo(
it.key,
it.value.read,
it.value.write,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
localInfoFeedTypes,
Kind3BasicRelaySetupInfo(
url = it.key,
read = it.value.read,
write = it.value.write,
feedTypes = localInfoFeedTypes,
relayStat = RelayStats.get(it.key),
)
}
.distinctBy { it.url }
.sortedBy { it.downloadCountInBytes }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
} else {
account.localRelays
.map {
val liveRelay = RelayPool.getRelay(it.url)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
RelaySetupInfo(
it.url,
it.read,
it.write,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
it.feedTypes,
Kind3BasicRelaySetupInfo(
url = it.url,
read = it.read,
write = it.write,
feedTypes = it.feedTypes,
relayStat = RelayStats.get(it.url),
)
}
.distinctBy { it.url }
.sortedBy { it.downloadCountInBytes }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
}
}
fun addRelay(relay: RelaySetupInfo) {
fun addAll(defaultRelays: Array<RelaySetupInfo>) {
hasModified = true
_relays.update {
defaultRelays.map {
Kind3BasicRelaySetupInfo(
url = it.url,
read = it.read,
write = it.write,
feedTypes = it.feedTypes,
relayStat = RelayStats.get(it.url),
)
}
.distinctBy { it.url }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
}
fun addRelay(relay: Kind3BasicRelaySetupInfo) {
if (relays.value.any { it.url == relay.url }) return
_relays.update { it.plus(relay) }
@@ -147,7 +156,7 @@ class Kind3RelayListViewModel : ViewModel() {
hasModified = true
}
fun deleteRelay(relay: RelaySetupInfo) {
fun deleteRelay(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.minus(relay) }
hasModified = true
}
@@ -157,48 +166,48 @@ class Kind3RelayListViewModel : ViewModel() {
hasModified = true
}
fun toggleDownload(relay: RelaySetupInfo) {
fun toggleDownload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(read = !relay.read)) }
hasModified = true
}
fun toggleUpload(relay: RelaySetupInfo) {
fun toggleUpload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(write = !relay.write)) }
hasModified = true
}
fun toggleFollows(relay: RelaySetupInfo) {
fun toggleFollows(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.FOLLOWS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleMessages(relay: RelaySetupInfo) {
fun toggleMessages(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PRIVATE_DMS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun togglePublicChats(relay: RelaySetupInfo) {
fun togglePublicChats(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PUBLIC_CHATS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleGlobal(relay: RelaySetupInfo) {
fun toggleGlobal(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.GLOBAL)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleSearch(relay: RelaySetupInfo) {
fun toggleSearch(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.SEARCH)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun togglePaidRelay(
relay: RelaySetupInfo,
relay: Kind3BasicRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
@@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -105,40 +105,22 @@ class Nip65RelayListViewModel : ViewModel() {
val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
_notificationRelays.update {
val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
}
@@ -31,15 +31,18 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -48,18 +51,24 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
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.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.events.EmptyTagList
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -70,6 +79,11 @@ fun RelayInformationDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val messages =
remember(relayBriefInfo) {
RelayStats.get(url = relayBriefInfo.url).messages.snapshot().values.sortedByDescending { it.time }.toImmutableList()
}
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
@@ -84,162 +98,204 @@ fun RelayInformationDialog(
),
) {
Surface {
val scrollState = rememberScrollState()
val color = mutableStateOf(Color.Transparent)
val context = LocalContext.current
Column(
modifier = Modifier.padding(10.dp).fillMaxSize().verticalScroll(scrollState),
LazyColumn(
modifier =
Modifier
.padding(10.dp)
.fillMaxSize(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton(onPress = { onClose() })
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StdPadding.fillMaxWidth(),
) {
Column {
RenderRelayIcon(
relayBriefInfo.displayUrl,
relayInfo.icon ?: relayBriefInfo.favIcon,
automaticallyShowProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
Spacer(modifier = DoubleHorzSpacer)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row { Title(relayInfo.name?.trim() ?: "") }
Row { SubtitleContent(relayInfo.description?.trim() ?: "") }
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton(onPress = { onClose() })
}
}
item {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StdPadding.fillMaxWidth(),
) {
Column {
RenderRelayIcon(
relayBriefInfo.displayUrl,
relayInfo.icon ?: relayBriefInfo.favIcon,
automaticallyShowProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
Section(stringResource(R.string.owner))
Spacer(modifier = DoubleHorzSpacer)
relayInfo.pubkey?.let {
DisplayOwnerInformation(it, accountViewModel) {
onClose()
nav(it)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row { Title(relayInfo.name?.trim() ?: "") }
Row { SubtitleContent(relayInfo.description?.trim() ?: "") }
}
}
}
item {
Section(stringResource(R.string.owner))
Section(stringResource(R.string.software))
relayInfo.pubkey?.let {
DisplayOwnerInformation(it, accountViewModel) {
onClose()
nav(it)
}
}
}
item {
Section(stringResource(R.string.software))
DisplaySoftwareInformation(relayInfo)
DisplaySoftwareInformation(relayInfo)
Section(stringResource(R.string.version))
Section(stringResource(R.string.version))
SectionContent(relayInfo.version ?: "")
SectionContent(relayInfo.version ?: "")
}
item {
Section(stringResource(R.string.contact))
Section(stringResource(R.string.contact))
Box(modifier = Modifier.padding(start = 10.dp)) {
relayInfo.contact?.let {
if (it.startsWith("https:")) {
ClickableUrl(urlText = it, url = it)
} else if (it.startsWith("mailto:") || it.contains('@')) {
ClickableEmail(it)
} else {
SectionContent(it)
}
}
}
}
item {
Section(stringResource(R.string.supports))
Box(modifier = Modifier.padding(start = 10.dp)) {
relayInfo.contact?.let {
if (it.startsWith("https:")) {
ClickableUrl(urlText = it, url = it)
} else if (it.startsWith("mailto:") || it.contains('@')) {
ClickableEmail(it)
} else {
SectionContent(it)
DisplaySupportedNips(relayInfo)
}
item {
relayInfo.fees?.admission?.let {
if (it.isNotEmpty()) {
Section(stringResource(R.string.admission_fees))
it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") }
}
}
relayInfo.payments_url?.let {
Section(stringResource(R.string.payments_url))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = it,
url = it,
)
}
}
}
item {
relayInfo.limitation?.let {
Section(stringResource(R.string.limitations))
val authRequiredText =
if (it.auth_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val paymentRequiredText =
if (it.payment_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val restrictedWritesText =
if (it.restricted_writes ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
Column {
SectionContent(
"${stringResource(R.string.message_length)}: ${it.max_message_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}",
)
SectionContent("${stringResource(R.string.filters)}: ${it.max_filters ?: 0}")
SectionContent(
"${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}")
SectionContent(
"${stringResource(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}",
)
SectionContent(
"${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.max_limit)}: ${it.max_limit ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringResource(R.string.auth)}: $authRequiredText")
SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText")
SectionContent("${stringResource(R.string.restricted_writes)}: $restrictedWritesText")
}
}
}
item {
relayInfo.relay_countries?.let {
Section(stringResource(R.string.countries))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.language_tags?.let {
Section(stringResource(R.string.languages))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.tags?.let {
Section(stringResource(R.string.tags))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.posting_policy?.let {
Section(stringResource(R.string.posting_policy))
Box(Modifier.padding(10.dp)) {
ClickableUrl(
it,
it,
)
}
}
}
Section(stringResource(R.string.supports))
DisplaySupportedNips(relayInfo)
relayInfo.fees?.admission?.let {
if (it.isNotEmpty()) {
Section(stringResource(R.string.admission_fees))
it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") }
}
item {
Section(stringResource(R.string.relay_error_messages))
}
relayInfo.payments_url?.let {
Section(stringResource(R.string.payments_url))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = it,
url = it,
items(messages) { msg ->
Row {
TranslatableRichTextViewer(
content =
remember {
"${timeAgo(msg.time, context)}, ${msg.type.name}: ${msg.message}"
},
canPreview = false,
quotesLeft = 0,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = color,
id = msg.hashCode().toString(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
relayInfo.limitation?.let {
Section(stringResource(R.string.limitations))
val authRequiredText =
if (it.auth_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val paymentRequiredText =
if (it.payment_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val restrictedWritesText =
if (it.restricted_writes ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
Column {
SectionContent(
"${stringResource(R.string.message_length)}: ${it.max_message_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}",
)
SectionContent("${stringResource(R.string.filters)}: ${it.max_filters ?: 0}")
SectionContent(
"${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}")
SectionContent(
"${stringResource(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}",
)
SectionContent(
"${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.max_limit)}: ${it.max_limit ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringResource(R.string.auth)}: $authRequiredText")
SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText")
SectionContent("${stringResource(R.string.restricted_writes)}: $restrictedWritesText")
}
}
relayInfo.relay_countries?.let {
Section(stringResource(R.string.countries))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.language_tags?.let {
Section(stringResource(R.string.languages))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.tags?.let {
Section(stringResource(R.string.tags))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.posting_policy?.let {
Section(stringResource(R.string.posting_policy))
Box(Modifier.padding(10.dp)) {
ClickableUrl(
it,
it,
)
}
Spacer(modifier = StdVertSpacer)
}
}
}
@@ -75,7 +75,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadableBytes(item.downloadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -101,7 +101,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadableBytes(item.uploadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -124,7 +124,7 @@ fun RelayStatusRow(
},
),
tint =
if (item.errorCount > 0) {
if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -132,7 +132,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadable(item.errorCount, "errors"),
text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -163,7 +163,7 @@ fun RelayStatusRow(
},
),
tint =
if (item.spamCount > 0) {
if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -171,7 +171,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadable(item.spamCount, "spam"),
text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -65,8 +66,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
val addedWSS = RelayUrlFormatter.normalize(url)
onNewRelay(BasicRelaySetupInfo(addedWSS))
onNewRelay(BasicRelaySetupInfo(RelayUrlFormatter.normalize(url), RelayStat()))
url = ""
}
},
@@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
@Composable
fun ClickableUrl(
@@ -39,6 +40,8 @@ fun ClickableUrl(
ClickableText(
text = text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onClick = {
runCatching {
val doubleCheckedUrl = if (url.contains("://")) url else "https://$url"
@@ -64,6 +64,7 @@ fun ExpandableRichTextViewer(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -99,6 +100,7 @@ fun ExpandableRichTextViewer(
modifier.align(Alignment.TopStart),
tags,
backgroundColor,
callbackUri,
accountViewModel,
nav,
)
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
fun LoadUrlPreview(
url: String,
urlText: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
) {
val automaticallyShowUrlPreview = remember { accountViewModel.settings.showUrlPreview.value }
@@ -61,7 +62,7 @@ fun LoadUrlPreview(
) { state ->
when (state) {
is UrlPreviewState.Loaded -> {
RenderLoaded(state, url, accountViewModel)
RenderLoaded(state, url, callbackUri, accountViewModel)
}
else -> {
ClickableUrl(urlText, url)
@@ -75,12 +76,13 @@ fun LoadUrlPreview(
fun RenderLoaded(
state: UrlPreviewState.Loaded,
url: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
) {
if (state.previewInfo.mimeType.type == "image") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlImage(url),
content = MediaUrlImage(url, uri = callbackUri),
roundedCorner = true,
accountViewModel = accountViewModel,
)
@@ -88,7 +90,7 @@ fun RenderLoaded(
} else if (state.previewInfo.mimeType.type == "video") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlVideo(url),
content = MediaUrlVideo(url, uri = callbackUri),
roundedCorner = true,
accountViewModel = accountViewModel,
)
@@ -121,14 +121,15 @@ fun RichTextViewer(
modifier: Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
Column(modifier = modifier) {
if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
} else {
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
}
}
}
@@ -267,7 +268,7 @@ fun RenderRegularPreview3() {
) { word, state ->
when (word) {
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, null, accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
@@ -291,16 +292,18 @@ private fun RenderRegular(
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
RenderRegular(content, tags) { word, state ->
RenderRegular(content, tags, callbackUri) { word, state ->
if (canPreview) {
RenderWordWithPreview(
word,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
@@ -321,9 +324,10 @@ private fun RenderRegular(
fun RenderRegular(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String? = null,
wordRenderer: @Composable (Segment, RichTextViewerState) -> Unit,
) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) }
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri)) }
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
@@ -412,12 +416,13 @@ private fun RenderWordWithPreview(
state: RichTextViewerState,
backgroundColor: MutableState<Color>,
quotesLeft: Int,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
when (word) {
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
@@ -542,15 +542,13 @@ fun GetVideoController(
}
}
if (event == Lifecycle.Event.ON_PAUSE) {
GlobalScope.launch(Dispatchers.Main) {
if (!keepPlaying.value) {
// Stops and releases the media.
controller.value?.let {
Log.d("PlaybackService", "Releasing Video from Pause $videoUri ")
it.stop()
it.release()
controller.value = null
}
if (!keepPlaying.value) {
// Stops and releases the media.
controller.value?.let {
Log.d("PlaybackService", "Releasing Video from Pause $videoUri ")
it.stop()
it.release()
controller.value = null
}
}
}
@@ -57,6 +57,7 @@ class MarkdownMediaRenderer(
val canPreview: Boolean,
val quotesLeft: Int,
val backgroundColor: MutableState<Color>,
val callbackUri: String? = null,
val accountViewModel: AccountViewModel,
val nav: (String) -> Unit,
) : MediaRenderer {
@@ -107,7 +108,7 @@ class MarkdownMediaRenderer(
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
val content = parser.parseMediaUrl(uri, eventTags = tags ?: EmptyTagList, startOfText)
val content = parser.parseMediaUrl(uri, eventTags = tags ?: EmptyTagList, startOfText, callbackUri)
if (canPreview) {
if (content != null) {
@@ -123,7 +124,7 @@ class MarkdownMediaRenderer(
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
} else {
renderInlineFullWidth(richTextStringBuilder) {
LoadUrlPreview(uri, title ?: uri, accountViewModel)
LoadUrlPreview(uri, title ?: uri, callbackUri, accountViewModel)
}
}
}
@@ -44,6 +44,7 @@ fun RenderContentAsMarkdown(
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -70,13 +71,14 @@ fun RenderContentAsMarkdown(
val renderer =
remember(content) {
MarkdownMediaRenderer(
content.take(100),
tags,
canPreview,
quotesLeft,
backgroundColor,
accountViewModel,
nav,
startOfText = content.take(100),
tags = tags,
canPreview = canPreview,
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
callbackUri = callbackUri,
accountViewModel = accountViewModel,
nav = nav,
)
}
@@ -396,9 +396,12 @@ fun AppNavigation(
LaunchedEffect(intentNextPage) {
if (actionableNextPage != null) {
actionableNextPage?.let {
navController.navigate(it) {
popUpTo(Route.Home.route)
launchSingleTop = true
val currentRoute = getRouteWithArguments(navController)
if (!isSameRoute(currentRoute, it)) {
navController.navigate(it) {
popUpTo(Route.Home.route)
launchSingleTop = true
}
}
actionableNextPage = null
}
@@ -759,6 +759,9 @@ fun RenderContentDVMThumb(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
// downloads user metadata to pre-load the NIP-65 relays.
val user = baseNote.author?.live()?.metadata?.observeAsState()
val card = observeAppDefinition(appDefinitionNote = baseNote)
LeftPictureLayout(
@@ -775,7 +778,16 @@ fun RenderContentDVMThumb(
.clip(QuoteBorder),
)
}
} ?: run { DisplayAuthorBanner(baseNote) }
} ?: run {
user?.value?.user?.let {
BannerImage(
it,
Modifier
.fillMaxSize()
.clip(QuoteBorder),
)
}
}
},
onTitleRow = {
Text(
@@ -642,6 +642,7 @@ private fun RenderRegularTextNote(
tags = tags,
backgroundColor = backgroundBubbleColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -655,6 +656,7 @@ private fun RenderRegularTextNote(
tags = EmptyTagList,
backgroundColor = backgroundBubbleColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -562,11 +562,13 @@ fun NoteBody(
nav = nav,
)
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = HalfDoubleVertSpacer)
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
if (!makeItShort) {
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = HalfDoubleVertSpacer)
DisplayZapSplits(noteEvent, false, accountViewModel, nav)
}
}
}
@@ -179,6 +179,7 @@ private fun OptionNote(
pollViewModel = pollViewModel,
nonClickablePrepend = {
RenderOptionAfterVote(
baseNote,
poolOption,
color,
canPreview,
@@ -217,6 +218,7 @@ private fun OptionNote(
@Composable
private fun RenderOptionAfterVote(
baseNote: Note,
poolOption: PollOption,
color: Color,
canPreview: Boolean,
@@ -276,7 +278,8 @@ private fun RenderOptionAfterVote(
Modifier,
tags,
backgroundColor,
poolOption.descriptor,
baseNote.idHex + poolOption.descriptor,
baseNote.toNostrUri(),
accountViewModel,
nav,
)
@@ -236,6 +236,7 @@ fun RenderAppDefinition(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -220,6 +220,7 @@ fun AudioHeader(
tags = tags,
backgroundColor = background,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -188,6 +188,7 @@ private fun RenderGitPatchEvent(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -291,6 +292,7 @@ private fun RenderGitIssueEvent(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -100,15 +100,16 @@ fun DisplayHighlight(
}
TranslatableRichTextViewer(
quote,
content = quote,
canPreview = canPreview && !makeItShort,
quotesLeft,
Modifier.fillMaxWidth(),
EmptyTagList,
backgroundColor,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = quote,
accountViewModel,
nav,
callbackUri = null,
accountViewModel = accountViewModel,
nav = nav,
)
DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav)
@@ -138,6 +138,7 @@ fun RenderNIP90ContentDiscoveryResponse(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.PollNoteEvent
@@ -65,22 +64,18 @@ fun RenderPoll(
val showReply by
remember(note) {
derivedStateOf {
noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
!makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
}
}
if (showReply) {
val replyingDirectlyTo =
remember(note) {
if (noteEvent is BaseTextNoteEvent) {
val replyingTo = noteEvent.replyingToAddressOrEvent()
if (replyingTo != null) {
val newNote = accountViewModel.getNoteIfExists(replyingTo)
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
newNote
} else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
}
val replyingTo = noteEvent.replyingToAddressOrEvent()
if (replyingTo != null) {
val newNote = accountViewModel.getNoteIfExists(replyingTo)
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
newNote
} else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
}
@@ -116,6 +111,7 @@ fun RenderPoll(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -115,6 +115,7 @@ fun RenderPrivateMessage(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -144,6 +145,7 @@ fun RenderPrivateMessage(
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -76,6 +76,7 @@ fun RenderReport(
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
quotesLeft = 1,
accountViewModel = accountViewModel,
nav = nav,
@@ -149,6 +149,7 @@ fun RenderTextEvent(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -119,6 +119,7 @@ fun RenderTextModificationEvent(
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
@@ -169,6 +169,7 @@ fun VideoDisplay(
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
+92 -75
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">क्यूआर क्रमचित्र के अभिमुख करें</string>
<string name="show_qr">क्यूआर क्रमचित्र दिखाएँ</string>
<string name="point_to_the_qr_code">क्यूआर क्रमचित्र के अभिमुख करें</string>
<string name="show_qr">क्यूआर क्रमचित्र दिखाएँ</string>
<string name="profile_image">परिचय चित्र</string>
<string name="your_profile_image">आपका परिचय चित्र</string>
<string name="scan_qr">क्यूआर क्रमचित्र को परखें</string>
<string name="scan_qr">क्यूआर क्रमचित्र को परखें</string>
<string name="show_anyway">फिर भी दिखाएँ</string>
<string name="post_was_hidden">इस प्रकाशित पत्र को छिपाया गया क्योंकि इसमें आपके आच्छाद्य उपयोगकर्ता अथवा शब्द उल्लेखित हैं</string>
<string name="post_was_flagged_as_inappropriate_by">प्रकाशित पत्र को मौन किया गया अथवा सूचित किया गया इनके द्वारा</string>
<string name="post_not_found">घटना पुष्ट किया जा रहा है अथवा आपके पुनःप्रसारक सूची में से प्राप्त नहीं</string>
<string name="post_not_found">घटना उपलब्ध किया जा रहा है अथवा आपके पुनःप्रसारक सूची में से प्राप्त नहीं</string>
<string name="channel_image">प्रणाली चित्र</string>
<string name="referenced_event_not_found">उद्धृत घटना अप्राप्त</string>
<string name="could_not_decrypt_the_message">सन्देश का अरहस्यीकरण असफल</string>
@@ -48,7 +48,7 @@
<string name="zaps">ज्साप</string>
<string name="view_count">दृष्ट संख्या</string>
<string name="boost">उद्धृत करें</string>
<string name="boosted">उद्धृत किया गया</string>
<string name="boosted">उद्धृत</string>
<string name="edited">सम्पादित</string>
<string name="edited_number">सम्पादन #%1$s</string>
<string name="original">मूल</string>
@@ -63,7 +63,7 @@
<string name="profile_banner">परिचय पृष्ठ चौडाचित्र</string>
<string name="payment_successful">भुगतान सफल</string>
<string name="error_parsing_error_message">अपक्रम संदेश परखने में अपक्रम</string>
<string name="following">" अनुचरण चालू"</string>
<string name="following">" का अनुचरण"</string>
<string name="followers">" अनुचर"</string>
<string name="profile">परिचय</string>
<string name="security_filters">सुरक्षा छलनियाँ</string>
@@ -86,12 +86,12 @@
<string name="about_us">"हमारा परिचय.. "</string>
<string name="what_s_on_your_mind">आपके मन में क्या है?</string>
<string name="post">पत्र प्रकाशन</string>
<string name="save">अभिलेखित करें</string>
<string name="save">अभिलेख करें</string>
<string name="create">बनाएँ</string>
<string name="cancel">निरस्त करें</string>
<string name="failed_to_upload_the_image">चित्र आरोहण असफल</string>
<string name="relay_address">पुनःप्रसारक पता</string>
<string name="posts">पत्र</string>
<string name="posts">्रकाशित पत्र</string>
<string name="bytes">अष्टक</string>
<string name="errors">अपक्रम</string>
<string name="errors_description">संयोजन अपक्रमों की संख्या इस सत्र में</string>
@@ -104,7 +104,7 @@
<string name="display_name">प्रदर्शन नाम</string>
<string name="my_display_name">मेरा प्रदर्शन नाम</string>
<string name="my_awesome_name">उष्ट्रपक्षी मक्बढिया</string>
<string name="welcome">स्वागतम उष्ट्रपक्षी</string>
<string name="welcome">स्वागतम उष्ट्रपक्षी!</string>
<string name="username">उपयोगकर्ता नाम</string>
<string name="my_username">मेरा उपयोगकर्ता नाम</string>
<string name="about_me">मेरा परिचय</string>
@@ -113,8 +113,8 @@
<string name="website_url">जालस्थान पता</string>
<string name="ln_address">लै॰जाल पता</string>
<string name="ln_url_outdated">लै॰जाल पता (पुराना)</string>
<string name="image_saved_to_the_gallery">चित्र क चित्रालय क्रमक में रख दिया गया</string>
<string name="failed_to_save_the_image">चित्र को रखने में असफल</string>
<string name="image_saved_to_the_gallery">चित्र का अभिलेखन किया गया चित्रालय क्रमक में</string>
<string name="failed_to_save_the_image">चित्र का अभिलेखन असफल</string>
<string name="upload_image">चित्र आरोहण</string>
<string name="uploading">आरोहण चल रहा है…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">उपयोगकर्ता का कोई लैटनिंग पता स्थापित नहीं जिसपर साट्स प्राप्त कर सके</string>
@@ -127,19 +127,19 @@
<string name="new_requests">नये अनुरोध</string>
<string name="blocked_users">बाधित उपयोगकर्ता</string>
<string name="new_threads">नये सूत्र</string>
<string name="conversations">वाद</string>
<string name="conversations">म्वाद</string>
<string name="notes">टीकाएँ</string>
<string name="replies">उत्तरें</string>
<string name="follows">"अनुचरण कर्ता है"</string>
<string name="replies">प्रतिवचन</string>
<string name="follows">"का अनुचरण"</string>
<string name="reports">"सूचनाएँ"</string>
<string name="more_options">अधिक विकल्प</string>
<string name="relays">" पुनःप्रसारक"</string>
<string name="website">जालस्थान</string>
<string name="lightning_address">लैटनिंग पता</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">NSEC विभेदक (आपका गुप्त पारणशब्द) क अनुकरण करता है टाँकाफलक में सुरक्षित रखने के लिए</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">एनसेक॰ विभेदक (आपका गुप्त पारणशब्द) क अनुकृति करता है टाँकाफलक में सुरक्षित रखने के लिए</string>
<string name="copy_private_key_to_the_clipboard">गुप्त कुंचिका की अनुकृति करें टाँकाफलक में</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">ख्याप्य कुंचिका की अनुकृति करता है टाँकाफलक में बाँटने के लिए</string>
<string name="copy_public_key_npub_to_the_clipboard">ख्याप्य कुंचिका (NPub) क अनुकरण करें टाँकाफलक में</string>
<string name="copy_public_key_npub_to_the_clipboard">ख्याप्य कुंचिका (एनपुब॰) क अनुकृति करें टाँकाफलक में</string>
<string name="send_a_direct_message">सीधा संदेश भेजें</string>
<string name="edits_the_user_s_metadata">उपयोगकर्ता के उपतथ्य का सम्पादन करता है</string>
<string name="follow">अनुचरण करें</string>
@@ -147,10 +147,10 @@
<string name="unblock">बाधा हटाएँ</string>
<string name="copy_user_id">उपयोगकर्ता विभेदक की अनुकृति करें</string>
<string name="unblock_user">उपयोगकर्ता बाधा हटाएँ</string>
<string name="npub_hex_username">"npub, उपयोगकर्ता नाम, लेख"</string>
<string name="npub_hex_username">"एनपुब॰, उपयोगकर्ता नाम, लेख"</string>
<string name="clear">रिक्त करें</string>
<string name="app_logo">क्रमक चिह्न</string>
<string name="nsec_npub_hex_private_key">nsec.. अथवा npub..</string>
<string name="nsec_npub_hex_private_key">एनसेक॰.. अथवा एनपुब॰..</string>
<string name="ncryptsec_password">कुंचिका को खोलने के लिए पारणशब्द</string>
<string name="show_password">पारणशब्द दिखाएँ</string>
<string name="hide_password">पारणशब्द छिपाएँ</string>
@@ -170,9 +170,9 @@
<string name="already_have_an_account">क्या नोस्ट्र लेखा पहले से है?</string>
<string name="create_a_new_account">नया लेखा बनाएँ</string>
<string name="generate_a_new_key">नयी कुंचिका बनाएँ</string>
<string name="loading_feed">सूचनावली प्राप्त किया जा रह है</string>
<string name="loading_account">लेखा प्राप्त किया जा रह है</string>
<string name="error_loading_replies">"उत्तरों को प्राप्त करने में अपक्रम "</string>
<string name="loading_feed">सूचनावली प्राप्त क जा रह है</string>
<string name="loading_account">लेखा जानकारी प्राप्त क जा रह है</string>
<string name="error_loading_replies">"प्रतिवचनों को प्राप्त करने में अपक्रम : "</string>
<string name="try_again">पुनः प्रयास करें</string>
<string name="feed_is_empty">सूचनावली रिक्त है।</string>
<string name="refresh">नवीकरण</string>
@@ -185,7 +185,7 @@
<string name="leave">निर्गमन</string>
<string name="unfollow">अनुचरण ना करें</string>
<string name="channel_created">प्रणाली बनायी गयी</string>
<string name="channel_information_changed_to">"प्रणाली सूचना बना दिया किया गया इसे"</string>
<string name="channel_information_changed_to">"प्रणाली जानकारी परिवर्तित की गयी"</string>
<string name="public_chat">सार्वजनिक चर्चा</string>
<string name="posts_received">पत्र प्राप्त</string>
<string name="remove">हटाएँ</string>
@@ -198,9 +198,9 @@
<string name="nip_05">नोस्ट्र पता</string>
<string name="never">कभी नहीं</string>
<string name="now">अभी</string>
<string name="h"></string>
<string name="m"></string>
<string name="d"></string>
<string name="h"> घं॰</string>
<string name="m"> मि॰</string>
<string name="d"> दि॰</string>
<string name="nudity">नग्नता</string>
<string name="profanity_hateful_speech">अपशब्द / द्वेषपूर्ण भाषण</string>
<string name="report_hateful_speech">द्वेषपूर्ण भाषण की सूचना दें</string>
@@ -208,10 +208,10 @@
<string name="others">अन्य</string>
<string name="mark_all_known_as_read">सब ज्ञात को पढ लिया चिह्नित करें</string>
<string name="mark_all_new_as_read">सब नये को पढ लिया चिह्नित करें</string>
<string name="mark_all_as_read">सब पढ लिया चिह्नित करें</string>
<string name="mark_all_as_read">सब को पढ लिया चिह्नित करें</string>
<string name="backup_keys">कुंचिकाएँ सुरक्षित रखें</string>
<string name="account_backup_tips2_md" tools:ignore="Typos">## कुंचिका सुरक्षार्थ अनुकृति तथा सुरक्षा सुझाव
\n\nआपकी लेखा एक गुप्त कुंचिका से सुरक्षित किया गया है। कुंचिका अक्षरों की एक लम्बी श्रृंखला है जो **nsec1** से आरम्भ होता है। जिस किसी के पास यह कुंचिका है वह पत्र प्रकाश कर सकेगा तथा आपका परिचय परिवर्तित कर सकेगा।
\n\nआपकी लेखा एक गुप्त कुंचिका से सुरक्षित किया गया है। कुंचिका अक्षरों की एक लम्बी श्रृंखला है जो **nsec1** से आरम्भ होता है। जिस किसी के पास यह कुंचिका है वह पत्र प्रकाशित कर सकेगा तथा आपका परिचय परिवर्तित कर सकेगा।
\n\n- आपकी गुप्त कुंचिका किसी भी जालस्थान अथवा क्रमक में **ना** डालें जिसपर आपका विश्वास ना हो।
\n- अमेथिस्ट क्रमलेख विकासकर्ता आपकी गुप्त कुंचिका **कभी नहीं** पूछेंगे।
\n- आपकी गुप्त कुंचिका की एक सुरक्षित अनुकृति **अवश्य** रखें अपनी लेखा पुनः प्राप्त करने के लिए। पारणशब्ध प्रबंधन क्रमक की पुनः प्रशंसा करते हैं।
@@ -220,7 +220,7 @@
\n\n यदि आप अपना पारणशब्द खो देते हैं, तो आप अपनी कुंचिका को पुनः प्राप्त नहीं कर सकते।
</string>
<string name="failed_to_encrypt_key">आपकी निजी कुंचिका का रहस्यीकरण असफल</string>
<string name="secret_key_copied_to_clipboard">गुप्त कुंचिका (nsec) क अनुकरण टाँकाफलक में डाला गया।</string>
<string name="secret_key_copied_to_clipboard">गुप्त कुंचिका (एनसेक॰) क अनुकृति टाँकाफलक में किया गया।</string>
<string name="copy_my_secret_key">मेरी गुप्त कुंचिका की अनुकृति करें</string>
<string name="encrypt_and_copy_my_secret_key">मेरी गुप्त कुंचिका का रहस्यीकरण तथा अनुकृति करें</string>
<string name="biometric_authentication_failed">प्रमाणीकरण असफल</string>
@@ -277,21 +277,21 @@
<string name="report_dialog_post_report_btn">सूचना प्रकाशित करें</string>
<string name="report_dialog_title">बाधित करें तथा सूचना दें</string>
<string name="block_only">बाधित करें</string>
<string name="bookmarks">स्मारकचिह्न</string>
<string name="bookmarks">स्मर्तव्य</string>
<string name="drafts">पाण्डुलिपियाँ</string>
<string name="private_bookmarks">निजी स्मारकचिह्नित वस्तु</string>
<string name="public_bookmarks">सार्वजनिक स्मारकचिह्नित वस्तु</string>
<string name="add_to_private_bookmarks">निजी स्मारकचिह्नित वस्तु सूची में जोडें</string>
<string name="add_to_public_bookmarks">सार्वजनिक स्मारकचिह्नित वस्तु सूची में जोडें</string>
<string name="remove_from_private_bookmarks">निजी स्मारकचिह्नित वस्तु सूची से हटाएँ</string>
<string name="remove_from_public_bookmarks">सार्वजनिक स्मारकचिह्नित वस्तु सूची से हटाएँ</string>
<string name="private_bookmarks">निजी स्मर्तव्य</string>
<string name="public_bookmarks">सार्वजनिक स्मर्तव्य</string>
<string name="add_to_private_bookmarks">निजी स्मर्तव्य सूची में जोडें</string>
<string name="add_to_public_bookmarks">सार्वजनिक स्मर्तव्य सूची में जोडें</string>
<string name="remove_from_private_bookmarks">निजी स्मर्तव्य सूची से हटाएँ</string>
<string name="remove_from_public_bookmarks">सार्वजनिक स्मर्तव्य सूची से हटाएँ</string>
<string name="wallet_connect_service">धनकोष संयोजन सेवा</string>
<string name="wallet_connect_service_explainer">एक नोस्ट्र रहस्य को अधिकृत करता है ज्साप भुगतान करने के लिए क्रमक से बाहर चले बिना। रहस्य को सुरक्षित रखें तथा एक निजी पुनःप्रसारक का प्रयोग करें हो सके तो।</string>
<string name="wallet_connect_service_pubkey">धनकोष संयोजन ख्याप्यकुंचिका</string>
<string name="wallet_connect_service_relay">धनकोष संयोजन पुनःप्रसारक</string>
<string name="wallet_connect_service_secret">धनकोष संयोजन रहस्य</string>
<string name="wallet_connect_service_show_secret">गुप्त कुंचिका दिखाएँ</string>
<string name="wallet_connect_service_secret_placeholder">nsec / षोडशांकरूप निजी कुंचिका</string>
<string name="wallet_connect_service_secret_placeholder">एनसेक॰ / षोडशांकरूप निजी कुंचिका</string>
<string name="pledge_amount_in_sats">प्रतिज्ञा मात्रा साट्स में</string>
<string name="post_poll">मतदान प्रकाशित करें</string>
<string name="poll_heading_required">अनिवार्य प्रपत्रस्थान :</string>
@@ -338,8 +338,8 @@
<string name="zap_type_nonzap_explainer">नोस्ट्र में कोई पदचिह्न नहीं, केवल लैटनिंग पर</string>
<string name="file_server">अभिलेख सेवासंगणक</string>
<string name="zap_forward_lnAddress">लै॰जाल पता अथवा @उपयोगकर्ता</string>
<string name="upload_server_relays_nip95">आपके पुनःप्रसारक (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">अभिलेख आपके पुनःप्रसारक द्वारा रखे जाते हैं। नया NIP: जाँच करें यदि वे सक्षम हैं</string>
<string name="upload_server_relays_nip95">आपके पुनःप्रसारक (निप॰-९५)</string>
<string name="upload_server_relays_nip95_explainer">अभिलेख आपके पुनःप्रसारक द्वारा रखे जाते हैं। नया निप॰: जाँच करें यदि वे अवलम्बन करते हैं</string>
<string name="connect_via_tor_short">टोर / ओर्बोट स्थापन</string>
<string name="connect_via_tor">आपकी ओर्बोट स्थापना द्वारा संयोजन करें</string>
<string name="do_you_really_want_to_disable_tor_title">क्या आपके ओर्बोट / टोर से संयोजन काट दें</string>
@@ -347,16 +347,16 @@
<string name="yes">हाँ</string>
<string name="no">नहीं</string>
<string name="follow_list_selection">अनुचरण सूची</string>
<string name="follow_list_kind3follows">सभी अनुचर</string>
<string name="follow_list_kind3follows">सभी अनुचरित</string>
<string name="follow_list_global">वैश्विक</string>
<string name="follow_list_mute_list">मौन सूची</string>
<string name="connect_through_your_orbot_setup_markdown">## टोर द्वारा संयोजन करें ओर्बोट के साथ
\n\n१. स्थापित करें [ओर्बोट](https://play.google.com/store/apps/details?id=org.torproject.android)
\n२. ओर्बोट आरम्भ करें
\n२. ओर्बोट चलाएँ
\n३. ओर्बोट में, सोक्स॰ द्वार का जाँच करें। मूलविकल्पतः ९०५० का प्रयोग करता है।
\n४. यदि आवश्यक हो तो ओर्बोट में द्वार परिवर्तित करें
\n५. सोक्स॰ द्वार की समाकृति करें इस पटल पर
\n६. सक्रिय करें घुण्डी दबाएँ ओर्बोट को मध्यस्थ के रूप में प्रयोग करने के लिए
\n६. सक्रिय करें सूचकयुक्त घुण्डी दबाएँ ओर्बोट को प्रतिनिधि के रूप में प्रयोग करने के लिए
</string>
<string name="orbot_socks_port">ओर्बोट सोक्स॰ द्वार</string>
<string name="invalid_port_number">द्वार अंक अमान्य</string>
@@ -372,7 +372,7 @@
<string name="reply_notify">सूचित करें : </string>
<string name="channel_list_join_conversation">संवाद में जुड जाएँ</string>
<string name="channel_list_user_or_group_id">उपयोगकर्ता अथवा झुण्ड विभेदक</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent अथवा षोडशांकरूप</string>
<string name="channel_list_user_or_group_id_demo">एनपुब॰, एनइवेण्ट॰ अथवा षोडशांकरूप</string>
<string name="channel_list_create_channel">बनाएँ</string>
<string name="channel_list_join_channel">जुडें</string>
<string name="today">आज</string>
@@ -383,7 +383,7 @@
<string name="content_warning_see_warnings">विषयवस्तु चेतावनियाँ सर्वदा दिखाएँ</string>
<string name="recommended_apps">पुनःप्रशंसित : </string>
<string name="filter_spam_from_strangers">अपरिचित जन के भेजे गये कचरालेखों को छलनी द्वारा हटाएँ</string>
<string name="warn_when_posts_have_reports_from_your_follows">चेतावनी दें जब पत्र सूचित किये गये हों आपके अनुचरों द्वारा</string>
<string name="warn_when_posts_have_reports_from_your_follows">चेतावनी दें जब पत्र सूचित किये गये हों आपके द्वारा अनुचरित व्यक्तियों से</string>
<string name="new_reaction_symbol">नया प्रतिक्रिया चिह्न</string>
<string name="no_reaction_type_setup_long_press_to_change">इस उपयोगकर्ता के लिए कोई प्रतिक्रिया प्रकार पूर्व चयनित नहीं। हृदयचिह्न घुण्डी पर दीर्घतः दबाएँ परिवर्तन करने के लिए</string>
<string name="zapraiser">ज्सापोपार्जन योजना</string>
@@ -399,7 +399,7 @@
<string name="version">संस्करण</string>
<string name="software">क्रमक</string>
<string name="contact">सम्पर्क</string>
<string name="supports">सक्षम NIPs</string>
<string name="supports">अवलम्बन किए हुए निप॰ सूची</string>
<string name="admission_fees">प्रवेश शुल्क</string>
<string name="payments_url">भुगतान जालपता</string>
<string name="limitations">परिसीमाएँ</string>
@@ -414,13 +414,13 @@
<string name="minimum_prefix">न्यूनतम उपसर्ग</string>
<string name="maximum_event_tags">अधिकतम घटना विषयसूचक</string>
<string name="content_length">विषयवस्तु लम्बाई</string>
<string name="minimum_pow">न्यूनतम PoW</string>
<string name="minimum_pow">न्यूनतम श्रमप्रमाण</string>
<string name="auth">प्रमाणीकरण</string>
<string name="payment">भुगतान</string>
<string name="cashu">काशयु लिपिखण्ड</string>
<string name="cashu">काशयु लिपिखण्ड</string>
<string name="cashu_redeem">चुकाएँ</string>
<string name="cashu_redeem_to_zap">ज्साप धनकोष को भेजें</string>
<string name="cashu_redeem_to_cashu">काशयु धनकोष में खोलें</string>
<string name="cashu_redeem_to_cashu">काशयु धनकोष में खोलें</string>
<string name="cashu_copy_token">लिपिखण्ड की अनुकृति करें</string>
<string name="no_lightning_address_set">कोई लैटनिंग पता स्थापित नहीं</string>
<string name="copied_token_to_clipboard">लिपिखण्ड की अनुकृति की गयी टाँकाफलक में</string>
@@ -445,9 +445,9 @@
<string name="add_sensitive_content_description">इसे दिखाने से पूर्व संवेदनशील विषयवस्तु चेतावनी जोडता है</string>
<string name="settings">स्थापना विकल्प</string>
<string name="connectivity_type_always">सर्वदा</string>
<string name="connectivity_type_wifi_only">केवल वैफै</string>
<string name="connectivity_type_wifi_only">केवल वैफै</string>
<string name="connectivity_type_never">कभी नहीं</string>
<string name="ui_feature_set_type_complete">सम्पन्न</string>
<string name="ui_feature_set_type_complete">सम्पूर्ण</string>
<string name="ui_feature_set_type_simplified">सरलीकृत</string>
<string name="system">यन्त्रव्यवस्था</string>
<string name="light">प्रकाशवान</string>
@@ -458,17 +458,17 @@
<string name="automatically_load_images_gifs">चित्र पूर्वीक्षण</string>
<string name="automatically_play_videos">चलचित्र का चालन</string>
<string name="automatically_show_url_preview">जालपता पूर्वीक्षण</string>
<string name="automatically_hide_nav_bars">निमग्न पृष्ठघुमाव</string>
<string name="automatically_hide_nav_bars">निमग्नकारी पृष्ठघुमाव</string>
<string name="automatically_hide_nav_bars_description">मार्गदर्शनपट्टियों को छिपाएँ पृष्ठघुमाव करने पर</string>
<string name="ui_style">प्रयोगमाध्यम शैली</string>
<string name="ui_style_description">पत्र प्रकाशन शैली का चयन करें</string>
<string name="load_image">चित्र प्राप्त करें</string>
<string name="spamming_users">कचरालेख प्रेषक</string>
<string name="muted_button">मौन किया गया। सुनने के लिए टाँकें</string>
<string name="muted_button">मौन किया गया। अमौन करने के लिए टाँकें</string>
<string name="mute_button">ध्वनि सक्रिय। मौन करने के लिए टाँकें</string>
<string name="search_button">स्थानीय तथा दूरस्थ अभिलेखों को खोजें</string>
<string name="nip05_verified">नोस्ट्र पता का सत्यापन सफल</string>
<string name="nip05_failed">नोस्ट्र पता का सत्यान असफल</string>
<string name="nip05_failed">नोस्ट्र पता का सत्यान असफल</string>
<string name="nip05_checking">नोस्ट्र पता का जाँच चल रहा है</string>
<string name="select_deselect_all">सब का चयन / अचयन करें</string>
<string name="default_relays">मूलविकल्प</string>
@@ -479,7 +479,7 @@
<string name="geohash_explainer">आपका भूगोलिक स्थान विभेदक जोडता है पत्र में। जनता जान जाएगी कि आप वर्तमान स्थान से ५ कि॰मे॰ (३ मी॰) की दूरी के अन्दर हैं</string>
<string name="add_sensitive_content_explainer">आपके विषयवस्तु दिखाने से पूर्व संवेदनशील विषयवस्तु चेतावनी जोडता है। यह आदर्श है किसी कार्यालय अनुचित विषयवस्तु के लिए अथवा जो कुछ लोगों के लिए आपत्तिजनक अथवा व्याकुल करनेवाला लग सकता है</string>
<string name="new_feature_nip17_might_not_be_available_title">नयी सुविधा</string>
<string name="new_feature_nip17_might_not_be_available_description">इस कार्यशैली सक्षम करने के लिए अमेथिस्ट के द्वारा NIP-17 संदेश (उपहारकोषयुक्त, आच्छादित सीधा तथा झुण्ड संदेश) भेजना पडेगा। यह NIP-17 नया है तथा अनेक ग्राहक इसे कार्यान्वित किया नहीं अब तक। सुनिश्चित करें कि प्राप्तकर्ता एक अनुकूल ग्राहक का प्रयोग कर रहे हैं।</string>
<string name="new_feature_nip17_might_not_be_available_description">इस कार्यशैली सक्षम करने के लिए अमेथिस्ट के द्वारा निप॰-१७ संदेश (उपहारकोषयुक्त, आच्छादित सीधा तथा झुण्ड संदेश) भेजना पडेगा। यह निप॰-१७ नया है तथा अनेक ग्राहक इसे कार्यान्वित किया नहीं अब तक। सुनिश्चित करें कि प्राप्तकर्ता एक अनुकूल ग्राहक का प्रयोग कर रहे हैं।</string>
<string name="new_feature_nip17_activate">सक्रिय करें</string>
<string name="messages_create_public_chat">सार्वजनिक</string>
<string name="messages_create_public_private_chat_desription">नया निजी अथवा सार्वजनिक झुण्ड</string>
@@ -494,12 +494,12 @@
<string name="paste_from_clipboard">टाँकाफलक से चिपकाएँ</string>
<string name="language_description">क्रमक के प्रयोगमाध्यम के लिए</string>
<string name="theme_description">अन्धकारमय, प्रकाशवान अथवा यन्त्रव्यवस्थित प्रदर्शनशैली</string>
<string name="automatically_load_images_gifs_description">स्वचालित रूप से चित्र तथा GIFs का अवरोहण करें</string>
<string name="automatically_play_videos_description">स्वचालित रूप से चलचित्र तथा GIFs चलाता है</string>
<string name="automatically_load_images_gifs_description">स्वचालित रूप से चित्र तथा जिफ॰ का अवरोहण करें</string>
<string name="automatically_play_videos_description">स्वचालित रूप से चलचित्र तथा जिफ॰ चलाता है</string>
<string name="automatically_show_url_preview_description">जालपता के पूर्वीक्षण दिखाएँ</string>
<string name="load_image_description">चित्रों का अवरोहण कब करें</string>
<string name="copy_to_clipboard">टाँकाफलक में अनुकृति करें</string>
<string name="copy_npub_to_clipboard">टाँकाफलक में npub अनुकरण करें</string>
<string name="copy_npub_to_clipboard">टाँकाफलक में एनपुब॰ अनुकृति करें</string>
<string name="copy_url_to_clipboard">टाँकाफलक में जालपता की अनुकृति करें</string>
<string name="copy_the_note_id_to_the_clipboard">टाँकाफलक में टीका विभेदक की अनुकृति करें</string>
<string name="created_at">तब बनाया गया</string>
@@ -548,14 +548,14 @@
<string name="error_dialog_pay_invoice_error">चालान का भुगतान नहीं कर पाए</string>
<string name="error_dialog_pay_withdraw_error">निकाल नहीं पाए</string>
<string name="error_parsing_nip47_title">धनकोष संयोजन स्थापित नहीं कर पाए</string>
<string name="error_parsing_nip47">NIP-47 संयोजन सूत्र परखने में अपक्रम। जाँच करें यदि यह सम्यक है अपने धनकोष सेवा प्रदाता के साथ : %1$s। अपक्रम : %2$s</string>
<string name="error_parsing_nip47_no_error">NIP-47 संयोजन सूत्र परखने में अपक्रम। जाँच करें यदि यह सम्यक है अपने धनकोष सेवा प्रदाता के साथ : %1$s।</string>
<string name="cashu_failed_redemption">काशयु नहीं चुका पाए</string>
<string name="error_parsing_nip47">निप॰-४७ संयोजन सूत्र परखने में अपक्रम। जाँच करें यदि यह सम्यक है अपने धनकोष सेवा प्रदाता के साथ : %1$s। अपक्रम : %2$s</string>
<string name="error_parsing_nip47_no_error">निप॰-४७ संयोजन सूत्र परखने में अपक्रम। जाँच करें यदि यह सम्यक है अपने धनकोष सेवा प्रदाता के साथ : %1$s।</string>
<string name="cashu_failed_redemption">काशयु नहीं चुका पाए</string>
<string name="cashu_failed_redemption_explainer_error_msg">टकसाल ने यह अपक्रम संदेश उपलब्ध किया : %1$s</string>
<string name="cashu_failed_redemption_explainer_already_spent">काशयु लिपिखण्ड का व्यय हो चुका।</string>
<string name="cashu_successful_redemption">काशयु प्राप्त</string>
<string name="cashu_failed_redemption_explainer_already_spent">काशयु लिपिखण्ड का व्यय हो चुका।</string>
<string name="cashu_successful_redemption">काशयु प्राप्त</string>
<string name="cashu_successful_redemption_explainer">%1$s साट्स भेजे गये आपके धनकोष में। (शुल्क : %2$s साट्स)</string>
<string name="cashu_no_wallet_found">कोई अनुकूल काशयु धनकोष उपलब्ध नहीं यन्त्र में</string>
<string name="cashu_no_wallet_found">कोई अनुकूल काशयु धनकोष उपलब्ध नहीं यन्त्र में</string>
<string name="error_unable_to_fetch_invoice">ग्राहक के सेवा संगणक से चालान नहीं लाया जा सका</string>
<string name="wallet_connect_pay_invoice_error_error">आपका धनकोष संयोजन प्रदाता ने यह अपक्रम लौटाया : %1$s</string>
<string name="could_not_connect_to_tor">टोर से जुड नहीं पाए</string>
@@ -565,17 +565,17 @@
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s का सुलझाव नहीं कर पाए। जाँच करें यदि आप संयोजित हैं, यदि सेवा संगणक चल रहा है तथा यदि लैटनिंग पता %2$s सम्यक है</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">%1$s का सुलझाव नहीं कर पाए। जाँच करें यदि आप संयोजित हैं, यदि सेवा संगणक चल रहा है तथा यदि लैटनिंग पता %2$s सम्यक है।\n\nअपवर्ग था : %3$s</string>
<string name="could_not_fetch_invoice_from">%1$s से चालान नहीं लाया जा सका</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">लैटनिंग पता से JSON परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">%1$s से JSON परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">लैटनिंग पता से जेसोन॰ परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">%1$s से जेसोन॰ परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">प्रत्याह्वान जालपता उपलब्ध नहीं उपयोगकर्ता के लैटनिंग पता सेवासंगणक की समाकृति में</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">प्रत्याह्वान जालपता उपलब्ध नहीं %1$s के उत्तर से</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup">लैटनिंग पता े चालान लान के JSON परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">%1$s े चालान लान के JSON परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup">लैटनिंग पता लाये गये चालान के जेसोन॰ परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">%1$s लाए गए चालान के जेसोन॰ परखनें में अपक्रम। उपयोगकर्ता की लैटनिंग स्थापना की जाँच करें</string>
<string name="incorrect_invoice_amount_sats_from_it_should_have_been">%2$s से चालान की मात्रा (%1$s साट्स) में दोष है। %3$s होना चाहिए था।</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error">ज्साप भेजने से पहले लैटनिंग चालान नहीं बना पाए। ग्राहक के लैटनिंग धनकोष ने यह अपक्रम की सूचना दी : %1$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">लैटनिंग चालान नहीं बना पाए। %1$s से संदेश : %2$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json">ज्साप भेजने से पहले लैटनिंग चालान नहीं बना पाए। परिणाम JSON में pr अम्श उपलब्ध नहीं।</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">%1$s से लैटनिंग चालान नहीं बना पाए। परिणाम JSON में pr अम्श उपलब्ध नहीं।</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json">ज्साप भेजने से पहले लैटनिंग चालान नहीं बना पाए। परिणाम जेसोन॰ में pr अम्श उपलब्ध नहीं।</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">%1$s से लैटनिंग चालान नहीं बना पाए। परिणाम जेसोन॰ में pr अम्श उपलब्ध नहीं।</string>
<string name="read_only_user">केवल पढनेवाला उपयोगकर्ता</string>
<string name="no_reactions_setup">कोई प्रतिक्रियाएँ स्थापित नहीं</string>
<string name="select_push_server">संयुक्तप्रेषण क्रमक का चयन करें</string>
@@ -585,7 +585,7 @@
<string name="push_server_none_explainer">प्रेषित सूचनाएँ अक्षम करता है</string>
<string name="push_server_uses_app_explainer">%1$s क्रमक का प्रयोग करता है</string>
<string name="push_server_install_app">प्रेषित सूचना स्थापन</string>
<string name="push_server_install_app_description"> प्रेषित सूचनाएँ प्राप्त करने के लिए, किसी भी क्रमक की स्थापना करें जो [संयुक्त प्रेषण](https://unifiedpush.org/) का अवलम्बन करता है, जैसे [Nfty](https://ntfy.sh/)।
<string name="push_server_install_app_description"> प्रेषित सूचनाएँ प्राप्त करने के लिए, किसी भी क्रमक की स्थापना करें जो [संयुक्त प्रेषण](https://unifiedpush.org/) का अवलम्बन करता है, जैसे [एनटीफै](https://ntfy.sh/)।
स्थापना पश्चात, जिस क्रमक का उपयोग करना चाहते हैं उसका चयन करें स्थापना विकल्पों में।
</string>
<string name="payment_required_title">%1$s से संदेश</string>
@@ -634,7 +634,7 @@
<string name="could_not_download_from_the_server">सेवासंगणक से आरोहणकृत चित्र चलचित्र का अवरोहण नहीं कर पाए</string>
<string name="could_not_prepare_local_file_to_upload">स्थानीय अभिलेख अनुकूल नहीं बना सके आरोहण के लिए : %1$s</string>
<string name="edit_draft">पाण्डुलिपि का सम्पादन करें</string>
<string name="login_with_qr_code">क्यूआर क्रमचित्र के साथ प्रवेशांकन करें</string>
<string name="login_with_qr_code">क्यूआर क्रमचित्र के साथ प्रवेशांकन करें</string>
<string name="route">मार्ग</string>
<string name="route_home">घर</string>
<string name="route_search">खोज</string>
@@ -671,26 +671,38 @@
<string name="cancel_zap_split">ज्साप विभाजन निरस्त करें</string>
<string name="add_content_warning">विषयवस्तु चेतावनी जोडें</string>
<string name="remove_content_warning">विषयवस्तु चेतावनी हटाएँ</string>
<string name="show_npub_as_a_qr_code">क्यूआर क्रमचित्र के रूप में npub को दिखाएँ</string>
<string name="show_npub_as_a_qr_code">क्यूआर क्रमचित्र के रूप में एनपुब॰ को दिखाएँ</string>
<string name="invalid_nip19_uri">अमान्य पता</string>
<string name="invalid_nip19_uri_description">अमेथिस्ट को एक वैश्विक वस्तु विभेदक प्राप्त हुआ खोलने के लिए परन्तु वह विभेदक अमान्य था : %1$s</string>
<string name="dm_relays_title">सी॰सं॰ आगतपेटिका पुनःप्रसारक</string>
<string name="dm_relays_through">पुनःप्रसारक : %1$s</string>
<string name="dm_relays_regular">साधारण पुनःप्रसारक प्रयुक्त</string>
<string name="dm_relays_not_found">आपके निजी आगतपेटिका पुनःप्रसारकों की स्थापना करें</string>
<string name="dm_relays_not_found_description">यह स्थापना विकल्प सब को सूचित करता है आपको सन्देश भेजने के लिए कौनसे पुनःप्रसारकों का प्रयोग करना चाहिए। इनके बिना आप कुछ सन्देश प्राप्त नहीं कर पाएँगे।</string>
<string name="dm_relays_not_found_examples">ये अच्छे विकल्प हैं :\n - inbox.nostr.wine (सशुल्क)\n - you.nostr1.com (निजी पुनःप्रसारक - सशुल्क)</string>
<string name="dm_relays_not_found_editing">निजी आगतपेटिका के रूप में १ - ३ पुनःप्रसारकों को जोडें। सी॰सं॰ आगतपेटिका पुनःप्रसारकों को सब से सन्देश स्वीकारना चाहिए पर उनका अवरोहण करने की अनुमति केवल आपको देना चाहिए।</string>
<string name="dm_relays_not_found_create_now">अभी स्थापना करें</string>
<string name="search_relays_title">खोज पुनःप्रसारक</string>
<string name="search_relays_not_found">आपके खोज पुनःप्रसारकों की स्थापना करें</string>
<string name="search_relays_not_found_description">खोज तथा उपयोगकर्ता सूचक जोडने के लिए स्पष्टतः रूपांकित पुनःप्रसारक सूची बनाने से इन परिणामों में शोधन होगा।</string>
<string name="search_relays_not_found_editing">विषयवस्तु खोजने के लिए अथवा उपयोगकर्ता सूचक जोडने में उपयोग करनें के लिए १ - ३ पुनःप्रसारकों को जोडें। सुनिश्चित करें कि आपके चयनित पुनःप्रसारक निप॰-५० को कार्यान्वित किये हैं।</string>
<string name="search_relays_not_found_examples">ये अच्छे विकल्प हैं :\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="relay_settings">पुनःप्रसारक स्थापना विकल्प</string>
<string name="public_home_section">सार्वजनिक मुख्य पुनःप्रसारक</string>
<string name="public_home_section_explainer">यह पुनःप्रसारक प्रकार आपके सभी विषयवस्तु रखता है। अमेथिस्ट आपके पत्रों को प्रकाशित करने के लिए यहाँ भेजेगा तथा अन्य लोग आपके विषयवस्तु ढूँढने के लिए इनका प्रयोग करेंगे। १ - ३ पुनःप्रसारकों को जोडें। ये व्यक्तिगत अथवा सशुल्क अथवा सार्वजनिक पुनःप्रसारक हो सकते हैं।</string>
<string name="public_notif_section">सार्वजनिक आगतपेटिका पुनःप्रसारक</string>
<string name="public_notif_section_explainer">यह पुनःप्रसारक प्रकार आपके प्रकाशित पत्रों को भेजे गये सभी प्रतिवचन, टिप्पणियाँ, इष्टसूचक तथा ज्सापों को प्राप्त करता है। १ - ३ पुनःप्रसारकों को जोडें तथा सुनिश्चित करें कि वे किसी से भी पत्र स्वीकारते हैं।</string>
<string name="private_inbox_section">सी॰सं॰ आगतपेटिका पुनःप्रसारक</string>
<string name="private_inbox_section_explainer">आपके निजी आगतपेटिका के रूप में १ - ३ पुनःप्रसारकों को जोडें। अन्य लोग इनका उपयोग करेंगे आपको सी॰सं॰ भेजने के लिए। सी॰सं॰ आगतपेटिका पुनःप्रसारकों को किसी से भी सन्देश स्वीकारना चाहिए पर उनका अवरोहण अनुमति केवल आपको देना चाहिए। ये अच्छे विकल्प हैं :\n - inbox.nostr.wine (सशुल्क)\n - you.nostr1.com (व्यक्तिगत पुनःप्रसारक - सशुल्क)</string>
<string name="private_outbox_section">निजी पुनःप्रसारक</string>
<string name="private_outbox_section_explainer">१ - ३ पुनःप्रसारकों को जोडें उन घटनाओं को रखने के लिए जो कोई अन्य नहीं देख सकता जैसे कि पाण्डुलिपियाँ अथवा क्रमक के स्थापना विकल्प। आदर्शतः ये पुनःप्रसारक स्थानीय होने चाहिए अथवा प्रत्येक उपयोगकर्ता के विषयवस्तु अवरोहण के पूर्व परिचय प्रमाणिकरण अनिवार्य होना चाहिए।</string>
<string name="kind_3_section">सामान्य पुनःप्रसारक</string>
<string name="kind_3_section_description">अमेथिस्ट इन पुनःप्रसारकों का प्रयोग करता है आपके पत्रों का अवरोहण करने के लिए।</string>
<string name="search_section">खोज पुनःप्रसारक</string>
<string name="search_section_explainer">पुनःप्रसारकों की सूची जिनको किसी विषयवस्तु अथवा उपयोगकर्ताओं को खोजने में प्रयोग करना चाहिए। कोई विकल्प नहीं तो उपयोगकर्ता सूचक जोडना तथा खोज निष्क्रिय होंगे। सुनिश्चित करें कि वे निप॰-५० को कार्यान्वित किये हैं।</string>
<string name="local_section">स्थानीय पुनःप्रसारक</string>
<string name="local_section_explainer">पुनःप्रसारकों की सूची जो इस यन्त्र पर चल रहे हैं।</string>
<string name="zap_the_devs_title">क्रमलखकों को ज्साप करें!</string>
<string name="zap_the_devs_title">क्रमलखकों को ज्साप करें!</string>
<string name="zap_the_devs_description">आपका दान हमारा सहायक है परिवर्तन लाने में। प्रत्येक साट गणनीय है!</string>
<string name="donate_now">दान करें अभी</string>
<string name="brought_to_you_by">आप तक लाया गया इनके द्वारा :</string>
@@ -715,11 +727,11 @@
<string name="accessibility_download_for_offline">अवरोहण</string>
<string name="accessibility_lyrics_on">संगीत पद्य दिखाएँ</string>
<string name="accessibility_lyrics_off">संगीत पद्य ना दिखाएँ</string>
<string name="accessibility_turn_on_sealed_message">वृत संदेश निष्क्रिय। वृत संदेश सक्रिय करने के लिए टाँकें</string>
<string name="accessibility_turn_off_sealed_message">वृत संदेश सक्रिय। वृत संदेश निष्क्रिय करने के लिए टाँकें</string>
<string name="accessibility_turn_on_sealed_message">समावृत संदेश निष्क्रिय। समावृत संदेश सक्रिय करने के लिए टाँकें</string>
<string name="accessibility_turn_off_sealed_message">समावृत संदेश सक्रिय। समावृत संदेश निष्क्रिय करने के लिए टाँकें</string>
<string name="accessibility_send">भेजें</string>
<string name="accessibility_play_username">उपयोगकर्ता नाम को ध्वनि के रूप में चलाएँ</string>
<string name="accessibility_scan_qr_code">क्यूआर क्रमचित्र परखें</string>
<string name="accessibility_scan_qr_code">क्यूआर क्रमचित्र परखें</string>
<string name="accessibility_navigate_to_alby">तृतीय पक्ष धनकोष प्रदाता आल्बी तक जाएँ</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">एक टीका पाण्डुलिपि को उत्तर नहीं दे सकते</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">एक टीका पाण्डुलिपि पर टीका नहीं लिख सकते</string>
@@ -727,4 +739,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">एक टीका पाण्डुलिपि को ज्साप नहीं कर सकते</string>
<string name="draft_note">टीका पाण्डुलिपि</string>
<string name="load_from_text">संदेश से</string>
<string name="dvm_looking_for_app">क्रमक ढूँढना चालू</string>
<string name="dvm_waiting_status">कार्य का अनुरोध किया गया। उत्तर की प्रतीक्षा में</string>
<string name="dvm_requesting_job">डीवीएम॰ से कार्य का अनुरोध किया जा रहा है</string>
<string name="nwc_payment_request">भुगतान अनुरोध भेजा गया। आपके धनकोष से पुष्टीकरण की प्रतीक्षा में</string>
<string name="dvm_waiting_to_confim_payment">डीवीएम॰ से भुगतान की पुष्टीकरण अथवा परिणाम भेजने की प्रतीक्षा में</string>
</resources>
+34
View File
@@ -15,6 +15,7 @@
<string name="group_picture">Csoport Kép</string>
<string name="explicit_content">Szókimondó tartalom</string>
<string name="spam">Spam</string>
<string name="spam_description">Ebből a csomópontból érkező spam események száma</string>
<string name="impersonation">Megszemélyesítés</string>
<string name="illegal_behavior">Illegális viselkedés</string>
<string name="other">Egyéb</string>
@@ -93,6 +94,7 @@
<string name="posts">Hozzászólások</string>
<string name="bytes">Bájtok</string>
<string name="errors">Hibák</string>
<string name="errors_description">Ebben a munkamenetben lévő kapcsolati hibák száma</string>
<string name="home_feed">Hírfolyamod</string>
<string name="private_message_feed">Privát Üzenetek listája</string>
<string name="public_chat_feed">Publikus Chat listája</string>
@@ -389,6 +391,8 @@
<string name="sats_to_complete">A ZapGyűjtés %1$s-nál. %2$s sats kell a célig</string>
<string name="read_from_relay">Olvassás a csomópontból</string>
<string name="write_to_relay">Írás a csomópontra</string>
<string name="write_to_relay_description">Ehhez a csomóponthoz küldött bájtok mennyisége, beleértve a szűrőket és az eseményeket</string>
<string name="read_from_relay_description">Ebből a csomópontból fogadott bájtok mennyisége, beleértve a szűrőket és az eseményeket</string>
<string name="an_error_occurred_trying_to_get_relay_information">Hiba történt a közvetítő információinak lekérése közben a %1$s -ról</string>
<string name="owner">Tulajdonos</string>
<string name="version">Verzió</string>
@@ -669,9 +673,34 @@
<string name="show_npub_as_a_qr_code">Az npub megjelenítése QR-kódként</string>
<string name="invalid_nip19_uri">Érvénytelen cím</string>
<string name="invalid_nip19_uri_description">Az Amethyst kapott egy URI-t a megnyitáshoz, de az érvénytelen volt: %1$s</string>
<string name="dm_relays_title">PÜ Bejövő Csomópontok</string>
<string name="dm_relays_through">Csomópontok: %1$s</string>
<string name="dm_relays_regular">Általános csomópontok használata</string>
<string name="dm_relays_not_found">Állítsd be a Privát postafiókod közvetítőit</string>
<string name="dm_relays_not_found_description">Ez a beállítás mindenkit tájékoztat, hogy melyik közvetítőt használod, amikor üzeneteket küldenek neked. Nélkülük néhány üzenetről lemaradhatsz.</string>
<string name="dm_relays_not_found_examples">Jó lehetőségek a következők:\n - inbox.nostr.wine (fizetős)\n - you.nostr1.com (személyes csomópontok - fizetős)</string>
<string name="dm_relays_not_found_editing">Adj hozzá 13 csomópontot, hogy privát postafiókodként szolgáljon. A PÜ Bejővő csomópontoknak el kell fogadniuk bármely üzenetet bárkitől, de azok letöltését csak Te általad teszik lehetővé.</string>
<string name="dm_relays_not_found_create_now">Állítsd be most</string>
<string name="search_relays_title">Kereső Csomópontok</string>
<string name="search_relays_not_found">Állítsd be a Kereséső csomópontjaidat</string>
<string name="search_relays_not_found_description">A kifejezetten kereséshez és felhasználói címkézéshez tervezett csomópontok listájának létrehozása javítja a találati eredményeket.</string>
<string name="search_relays_not_found_editing">Adj hozzá 13 csomópontot tartalom kereséshez vagy a felhasználók címkézéséhez. Győződj meg arról, hogy a kiválasztott csomópontok alkalmazzák a NIP-50-et</string>
<string name="search_relays_not_found_examples">Jó lehetőségek a következők:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="relay_settings">Csomópont Beállítások</string>
<string name="public_home_section">Publikus Otthoni Csomópontok</string>
<string name="public_home_section_explainer">Ez a csomópont típus az összes tartalmat tárolja. Az Amethyst ide küldi a bejegyzéseidet, mások pedig ezeket a csomópontokat fogják használni, hogy a tartalmadat megtalálják. Adj hozzá 1-3 csomópontot. Lehetnek saját, fizetős vagy nyilvános csomópontok.</string>
<string name="public_notif_section">Publikus Bejövő Csomópontok</string>
<string name="public_notif_section_explainer">Ez a csomópont típus fogadja a bejegyzéseidre adott összes választ, megjegyzést, tetszésnyilvánítást és visszajelzést. Adj hozzá 13 csomópontot, és győződj meg arról, hogy ezek bárkitől fogadnak bejegyzéseket.</string>
<string name="private_inbox_section">PÜ Bejövő Csomópontok</string>
<string name="private_inbox_section_explainer">Adj hozzá 13 csomópontot, hogy privát postafiókodként szolgáljon. Mások ezeket a csomópontokat használják, hogy neked PÜ-eket küldjenek. A PÜ Bejövő csomópontoknak bárkitől kell fogadniuk bármely üzenetet, de azok letöltését csak a Te részedre teszik lehetővé. Jó lehetőségek a következők:\n - inbox.nostr.wine (fizetős)\n - you.nostr1.com (személyes csomópontok - fizetős)</string>
<string name="private_outbox_section">Privát Csomópontok</string>
<string name="private_outbox_section_explainer">Adj hozzá 13 csomópontot, hogy olyan eseményeket tároljon, amelyeket senki más nem láthat, például a piszkozataidat és/vagy az alkalmazásbeállításaidat. Ideális esetben ezek a csomópontok vagy helyiek, vagy az egyes felhasználók tartalmának letöltése előtt hitelesítést igényelnek.</string>
<string name="kind_3_section">Általános Csomópontok</string>
<string name="kind_3_section_description">Az Amethyst ezeket a csomópontokat a bejegyzések letöltésére használja.</string>
<string name="search_section">Kereső Csomópontok</string>
<string name="search_section_explainer">A tartalom vagy a felhasználók kereséséhez használt csomópontok listája. A címkézés és a keresés nem működik, ha itt nincsenek megadott lehetőségek. Győződj meg arról, hogy alkalmazzák a NIP-50-et.</string>
<string name="local_section">Helyi Csomópontok</string>
<string name="local_section_explainer">Ezen eszközön futó csomópontok listája.</string>
<string name="zap_the_devs_title">Zap a fejlesztőknek!</string>
<string name="zap_the_devs_description">Az adományod hozzájárul ahhoz, hogy változást érjünk el. Minden sat számít!</string>
<string name="donate_now">Adományozz Most</string>
@@ -709,4 +738,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">A bejegyzésvázlatra nem lehet zap-pelni</string>
<string name="draft_note">Bejegyzéspiszkozat</string>
<string name="load_from_text">Tőle üzenet</string>
<string name="dvm_looking_for_app">Alkalmazást keresek</string>
<string name="dvm_waiting_status">Feladat kérve, válaszra várunk</string>
<string name="dvm_requesting_job">Feladatot kér a DVM-től</string>
<string name="nwc_payment_request">Fizetési kérelem elküldve, megerősítésére vár a tárcádtól</string>
<string name="dvm_waiting_to_confim_payment">Várakozás a DVM-re a fizetés megerősítésére vagy az eredmények elküldésére</string>
</resources>
+39 -1
View File
@@ -15,13 +15,14 @@
<string name="group_picture">รูปภาพกลุ่ม</string>
<string name="explicit_content">เนื้อหาที่มีความรุนแรง</string>
<string name="spam">สแปม</string>
<string name="spam_description">สแปมอีเว้นถูกรับมาจาก relay นี้</string>
<string name="impersonation">เลียนแบบ</string>
<string name="illegal_behavior">พฤติกรรมที่ผิดกฎหมาย</string>
<string name="other">อื่นๆ</string>
<string name="unknown">ไม่ทราบ</string>
<string name="relay_icon">รีเลย์ไอคอน</string>
<string name="unknown_author">ไม่ทราบผู้เขียน</string>
<string name="copy_text">คัดลอกข้อความ</string>
<string name="copy_text">คัดลอกข้อความ</string>
<string name="copy_user_pubkey">คัดลอก ID ผู้เขียน</string>
<string name="copy_note_id">คัดลอก ID โน้ต</string>
<string name="broadcast">เพยแพร่</string>
@@ -93,6 +94,7 @@
<string name="posts">โพสต์</string>
<string name="bytes">ไบต์</string>
<string name="errors">ข้อผิดพลาด</string>
<string name="errors_description">เกิดข้อผิดพลาดในการเชื่อมต่อ</string>
<string name="home_feed">ฟีดเริ่มต้น</string>
<string name="private_message_feed">ฟีดข้อความส่วนตัว</string>
<string name="public_chat_feed">ฟีดข้อความ</string>
@@ -389,6 +391,8 @@
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats เพื่อไปถึงเป้าหมาย</string>
<string name="read_from_relay">อ่านจาก Relay</string>
<string name="write_to_relay">เขียนบน Relay</string>
<string name="write_to_relay_description">จำนวนข้อมูลในหน่วยไบต์ที่ถูกส่งไปยังรีเลย์นี้ รวมถึงฟิลเตอร์และอีเว้นต่างๆ</string>
<string name="read_from_relay_description">จำนวนข้อมูลในหน่วยไบต์ที่ถูกรับมาจากรีเลย์นี้ รวมถึงฟิลเตอร์และอีเว้นต่างๆ</string>
<string name="an_error_occurred_trying_to_get_relay_information">เกิดข้อผิดพลาดในการรับข้อมูลจาก %1$s</string>
<string name="owner">เจ้าของ</string>
<string name="version">เวอร์ชัน</string>
@@ -428,6 +432,7 @@
<string name="are_you_sure_you_want_to_log_out">การออกจากระบบจะลบข้อมูลทั้งหมดของคุณ ตรวจสอบให้แน่ใจว่าได้สํารองข้อมูล private key ไว้เพื่อหลีกเลี่ยงการสูญเสียบัญชีของคุณ คุณต้องการดําเนินการต่อหรือไม่?</string>
<string name="followed_tags">ติดตามแท็ก</string>
<string name="relay_setup">รีเลย์</string>
<string name="discover_content">ค้นหาโน๊ตใหม่ ๆ</string>
<string name="discover_marketplace">ตลาด</string>
<string name="discover_live">ถ่ายทอดสด</string>
<string name="discover_community">ชุมชน</string>
@@ -666,6 +671,34 @@
<string name="show_npub_as_a_qr_code">แสดง npub เป็น qr code</string>
<string name="invalid_nip19_uri">ที่อยู่ไม่ถูกต้อง</string>
<string name="invalid_nip19_uri_description">Amethyst ได้รับ URI แต่ URI นั้นไม่ถูกต้อง: %1$s</string>
<string name="dm_relays_title">รีเลย์ขาเข้าของ DM</string>
<string name="dm_relays_through">รีเลย์: %1$s</string>
<string name="dm_relays_regular">ใช้รีเลย์ทั่วไป</string>
<string name="dm_relays_not_found">ตั้งค่ารีเลย์ส่วนตัวสำหรับรับข้อมูล</string>
<string name="dm_relays_not_found_description">การตั้งค่านี้จะแจ้งให้ทุกคนทราบว่าควรใช้รีเลย์ใดในการส่งข้อความถึงคุณ หากไม่มีการตั้งค่านี้ คุณอาจพลาดข้อความบางส่วนได้</string>
<string name="dm_relays_not_found_examples">ตัวเลือกที่ดี ได้แก่:\n- inbox.nostr.wine (เสียค่าบริการ)\n- you.nostr1.com (รีเลย์ส่วนตัว - เสียค่าบริการ)</string>
<string name="dm_relays_not_found_editing">ใส่รีเลย์ 1-3 ตัวเพื่อทำหน้าที่เป็นกล่องขาเข้าส่วนตัวของคุณ รีเลย์กล่องขาเข้า DM ควรยอมรับข้อความจากทุกคน แต่อนุญาตให้คุณดาวน์โหลดได้เท่านั้น</string>
<string name="dm_relays_not_found_create_now">ตั้งค่าตอนนี้เลย</string>
<string name="search_relays_title">ค้นหารีเลย์</string>
<string name="search_relays_not_found">ตั้งค่ารีเลย์เพื่อใช้ค้นหา</string>
<string name="search_relays_not_found_description">การสร้างรายชื่อรีเลย์ที่ออกแบบมาเฉพาะสำหรับการค้นหาและการแท็กผู้ใช้จะช่วยปรับปรุงผลลัพธ์เหล่านี้ได้</string>
<string name="search_relays_not_found_editing">ใส่รีเลย์ 1-3 ตัวเพื่อใช้ในการค้นหาข้อมูลหรือแท็กผู้ใช้ ตรวจสอบให้แน่ใจว่ารีเลย์ที่คุณเลือกใช้รองรับ NIP-50</string>
<string name="search_relays_not_found_examples">ตัวเลือกที่ดี ได้แก่:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="relay_settings">ตั้งค่ารีเลย์</string>
<string name="public_home_section">รีเลย์สาธารณะสำหรับหน้า Home</string>
<string name="public_home_section_explainer">รีเลย์ประเภทนี้จะเก็บเนื้อหาทั้งหมดของคุณ Amethyst จะส่งโพสต์ของคุณไปยังรีเลย์เหล่านี้ และคนอื่น ๆ จะใช้รีเลย์เหล่านี้เพื่อค้นหาเนื้อหาของคุณ ใส่รีเลย์ 1-3 ตัว ซึ่งสามารถเป็นรีเลย์ส่วนตัว รีเลย์ที่ต้องเสียค่าบริการ หรือรีเลย์สาธารณะก็ได้</string>
<string name="public_notif_section">รีเลย์ขาเข้าสาธารณะ</string>
<string name="public_notif_section_explainer">รีเลย์ประเภทนี้จะรับการตอบกลับ ความคิดเห็น การถูกใจ และการส่ง \"zap\" ทั้งหมดไปยังโพสต์ของคุณ ใส่รีเลย์ 1-3 ตัว และตรวจสอบให้แน่ใจว่ารีเลย์เหล่านี้ยอมรับโพสต์จากทุกคน</string>
<string name="private_inbox_section">รีเลย์ขาเข้าของ DM</string>
<string name="private_inbox_section_explainer">ใส่รีเลย์ 1-3 ตัวเพื่อทำหน้าที่เป็นกล่องขาเข้าส่วนตัวของคุณ คนอื่นจะใช้รีเลย์เหล่านี้ในการส่งข้อความ DM ถึงคุณ รีเลย์กล่องขาเข้า DM ควรยอมรับข้อความจากทุกคน แต่อนุญาตให้คุณดาวน์โหลดได้เท่านั้น ตัวเลือกที่ดี ได้แก่:\n - inbox.nostr.wine (เสียค่าบริการ)\n - you.nostr1.com (รีเลย์ส่วนตัว - เสียค่าบริการ)</string>
<string name="private_outbox_section">รีเลย์ส่วนตัว</string>
<string name="private_outbox_section_explainer">ใส่รีเลย์ 1-3 ตัวเพื่อเก็บเหตุการณ์ที่ไม่มีใครสามารถเห็นได้ เช่น ร่างโพสต์ และการตั้งค่าแอปของคุณ รีเลย์เหล่านี้ควรเป็นรีเลย์ภายในเครื่องหรือรีเลย์ที่ต้องมีการยืนยันตัวตนก่อนที่จะดาวน์โหลดเนื้อหาของผู้ใช้แต่ละคน</string>
<string name="kind_3_section">รีเลย์ทั่วไป</string>
<string name="kind_3_section_description">Amethyst ใช้รีเลย์เหล่านี้เพื่อดาวน์โหลดโพสต์สำหรับคุณ</string>
<string name="search_section">ค้นหารีเลย์</string>
<string name="search_section_explainer">รายการรีเลย์ที่จะใช้เมื่อค้นหาข้อมูลหรือผู้ใช้ การแท็กและการค้นหาจะไม่ทำงานหากไม่มีตัวเลือก ตรวจสอบให้แน่ใจว่ารีเลย์เหล่านี้รองรับ NIP-50</string>
<string name="local_section">รีเลย์ในพื้นที่ของคุณ</string>
<string name="local_section_explainer">รายการรีเลย์ที่กำลังทำงานอยู่ในอุปกรณ์นี้</string>
<string name="zap_the_devs_title">zap ให้นักพัฒนา</string>
<string name="zap_the_devs_description">การบริจาคของคุณช่วยให้เราสร้างความแตกต่าง ทุก sat มีค่า!</string>
<string name="donate_now">บริจาค</string>
@@ -703,4 +736,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">ไม่สามารถ zap โน้ตฉบับร่างได้</string>
<string name="draft_note">โน๊ตฉบับร่าง</string>
<string name="load_from_text">From Msg</string>
<string name="dvm_looking_for_app">กำลังค้นหาแอพพลิเคชั่น Dvm</string>
<string name="dvm_waiting_status">ส่งคำขอเรียบร้อย และกำลังรอการตอบกลับ</string>
<string name="dvm_requesting_job">กำลังรองานจาก DVM</string>
<string name="nwc_payment_request">คำขอชำระเงินถูกส่งแล้ว รอการยืนยันจาก wallet ของคุณ</string>
<string name="dvm_waiting_to_confim_payment">รอการยืนยันการชำระเงินหรือผลลัพธ์จาก DVM</string>
</resources>
+4 -3
View File
@@ -26,7 +26,7 @@
<string name="copy_user_pubkey">复制作者@npub</string>
<string name="copy_note_id">复制笔记ID</string>
<string name="broadcast">广播</string>
<string name="timestamp_it">获取 OpenTimestamps</string>
<string name="timestamp_it">获取公开时间戳</string>
<string name="timestamp_pending">OpenTimestamps:待确认</string>
<string name="timestamp_pending_short">OTS:待定</string>
<string name="request_deletion">请求删除</string>
@@ -407,6 +407,7 @@
<string name="languages">语言</string>
<string name="tags">标签</string>
<string name="posting_policy">发布政策</string>
<string name="relay_error_messages">该中继的错误和通知</string>
<string name="message_length">消息长度</string>
<string name="subscriptions">订阅</string>
<string name="filters">筛选器</string>
@@ -695,7 +696,7 @@
<string name="private_inbox_section">私信收件箱中继</string>
<string name="private_inbox_section_explainer">设置 1 ~ 3 个中继作为您的私人收件箱。其他人将使用这些中继向您发送私信。需要确保这些收件箱中继能够接受来自任何人的任何私信消息,但只允许您读取这些消息。示例:\n - inbox.nostr.wine(付费)\n - you.nostr1.com(个人专用中继 - 付费)</string>
<string name="private_outbox_section">私人中继</string>
<string name="private_outbox_section_explainer">设置 1 ~ 3 个中继用于保存其他人无法看到的事件,例如的草稿和软件设置。理想情况下,这些应该是本地中继,或者是在获取用户内容时需要经过身份验证的中继。</string>
<string name="private_outbox_section_explainer">设置 1 ~ 3 个中继用于保存其他人无法看到的事件,例如的草稿和软件设置。理想情况下,这些应该是本地中继,或者是在获取用户内容时需要经过身份验证的中继。</string>
<string name="kind_3_section">通用中继</string>
<string name="kind_3_section_description">Amethyst 会通过这些中继为您获取帖子。</string>
<string name="search_section">搜索中继</string>
@@ -711,7 +712,7 @@
<string name="thank_you">谢谢!</string>
<string name="max_limit">最大上限</string>
<string name="restricted_writes">写入限制</string>
<string name="forked_from"></string>
<string name="forked_from">Fork 來源</string>
<string name="forked_tag">复刻</string>
<string name="git_repository">Git 仓库: %1$s</string>
<string name="git_web_address">网址</string>
@@ -15,6 +15,7 @@
<string name="group_picture">群組圖片</string>
<string name="explicit_content">露骨內容</string>
<string name="spam">垃圾郵件</string>
<string name="spam_description">來自該中繼的垃圾事件數量</string>
<string name="impersonation">冒充</string>
<string name="illegal_behavior">非法行為</string>
<string name="other">其它</string>
@@ -52,6 +53,7 @@
<string name="edited_number">編輯 #%1$s</string>
<string name="original">原版</string>
<string name="quote">引用</string>
<string name="fork">Fork</string>
<string name="propose_an_edit">提議編輯</string>
<string name="new_amount_in_sats">新的聰金額</string>
<string name="add">添加</string>
@@ -92,6 +94,7 @@
<string name="posts">貼文</string>
<string name="bytes">位元組</string>
<string name="errors">錯誤</string>
<string name="errors_description">此會話中產生的連線錯誤數量</string>
<string name="home_feed">主頁</string>
<string name="private_message_feed">私信</string>
<string name="public_chat_feed">公開聊天</string>
@@ -389,6 +392,8 @@
<string name="sats_to_complete">Zapraiser 位於 %1$s。距離目標%2$s聰</string>
<string name="read_from_relay">從中繼器讀取</string>
<string name="write_to_relay">寫入到中繼器</string>
<string name="write_to_relay_description">發送到此中繼的數據量,包括過濾和事件</string>
<string name="read_from_relay_description">從此中繼接收的數據量,包括過濾和事件</string>
<string name="an_error_occurred_trying_to_get_relay_information">嘗試從 %1$s 獲取中繼器信息時出錯</string>
<string name="owner">機主</string>
<string name="version">版本</string>
@@ -428,6 +433,7 @@
<string name="are_you_sure_you_want_to_log_out">登出將刪除你的本地信息。請確保備份你的私鑰以避免失去你的帳戶。你想要繼續嗎?</string>
<string name="followed_tags">以關注的標籤</string>
<string name="relay_setup">中繼器</string>
<string name="discover_content">筆記發現</string>
<string name="discover_marketplace">市場</string>
<string name="discover_live">直播</string>
<string name="discover_community">社群</string>
@@ -668,14 +674,34 @@
<string name="show_npub_as_a_qr_code">將 npub 顯示爲二維碼</string>
<string name="invalid_nip19_uri">地址無效</string>
<string name="invalid_nip19_uri_description">Amethyst 收到了要打開的 URI,但該 URI 無效:%1$s</string>
<string name="dm_relays_title">私信收件匣中繼</string>
<string name="dm_relays_through">中繼器 %1$s</string>
<string name="dm_relays_regular">正在使用常規中繼器</string>
<string name="dm_relays_not_found">設定你的私人收件匣中繼</string>
<string name="dm_relays_not_found_description">此設定讓大家知道向你發送訊息時要使用哪些中繼。如果沒有它們,你可能會錯過一些訊息。</string>
<string name="dm_relays_not_found_examples">不錯的選擇是:\n - inbox.nostr.wine(付費)\n - you.nostr1.com(個人中繼 - 付費)</string>
<string name="dm_relays_not_found_editing">輸入 1-3 個中繼作為你的私人收件匣。 私信收件匣中繼應該接受任何人的任何訊息,但只允許你下載它們。</string>
<string name="dm_relays_not_found_create_now">立即設定</string>
<string name="search_relays_title">搜索中繼器</string>
<string name="search_relays_not_found">設置你的搜索中繼器</string>
<string name="search_relays_not_found_description">建立專為搜索和標記用戶設計的中繼列表能夠改善這些結果。</string>
<string name="search_relays_not_found_editing">輸入 1-3 個中繼以在搜尋內容或標記用戶時使用。確保你選擇的中繼器實施 NIP-50</string>
<string name="search_relays_not_found_examples">不錯的選擇是:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="relay_settings">中繼器設置</string>
<string name="public_home_section">公開主頁中繼</string>
<string name="public_home_section_explainer">這種中繼將保存所有你的內容。Amethyst 會將你的帖子發送到此處,而他人則將使用這些中繼查找你的內容。輸入 1-3 個中繼。它們可以是個人中繼、付費中繼或公開中繼。</string>
<string name="public_notif_section">公開收件匣中繼</string>
<string name="public_notif_section_explainer">這種中繼接收所有對你的帖子的回覆、評論、點贊和打閃。輸入 1-3 個中繼並確認它們接收來自任何人的事件。</string>
<string name="private_inbox_section">私信收件匣中繼</string>
<string name="private_inbox_section_explainer">輸入 1-3 個中繼作爲你的私人收件匣。他人將使用這些中繼向你發送私信。私信收件匣中繼應該接受來自任何人的訊息,但只允許你下載它們。不錯的選擇是:\n - inbox.nostr.wine (付費)\n - you.nostr1.com (私人中繼 - 付費)</string>
<string name="private_outbox_section">私人中繼</string>
<string name="private_outbox_section_explainer">輸入 1-3 個中繼用於存儲別人看不到的事件,例如你的草稿和/或應用設置。理想情況下,這些中繼要么是本地的,要么需要在下載每個用戶的內容之前進行驗證。</string>
<string name="kind_3_section">一般中繼器</string>
<string name="kind_3_section_description">Amethyst 使用這些中繼爲你下載帖子。</string>
<string name="search_section">搜索中繼器</string>
<string name="search_section_explainer">搜尋內容或用戶時使用的中繼列表。如果沒有可用選項,標記和搜索將無法運作。確保他們實施 NIP-50。</string>
<string name="local_section">本地中繼器</string>
<string name="local_section_explainer">在該設備上運行的中繼器列表。</string>
<string name="zap_the_devs_title">打閃開發人員</string>
<string name="zap_the_devs_description">你的捐贈幫助我們做出不同的貢獻。 每個聰都很重要!</string>
<string name="donate_now">立即捐款</string>
@@ -684,7 +710,12 @@
<string name="version_name">版本 %1$s</string>
<string name="thank_you">謝謝!</string>
<string name="max_limit">最大上限</string>
<string name="restricted_writes">寫入限制</string>
<string name="forked_from">Forked from</string>
<string name="forked_tag">FORK</string>
<string name="git_repository">Git Repository: %1$s</string>
<string name="git_web_address">網址</string>
<string name="git_clone_address">Clone:</string>
<string name="existed_since">OTS%1$s</string>
<string name="ots_info_title">時間戳證明</string>
<string name="ots_info_description">有在 %1$s 之前的某個時間簽署了此帖子的證明。此證明是在那個日期和時間在比特幣區塊鏈中記錄的時間戳。</string>
@@ -708,4 +739,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">無法打閃筆記草稿</string>
<string name="draft_note">筆記草稿</string>
<string name="load_from_text">來自消息</string>
<string name="dvm_looking_for_app">正在查找應用程式</string>
<string name="dvm_waiting_status">數據作業已申請,正在等待回覆</string>
<string name="dvm_requesting_job">正在向 DVM 申請數據作業</string>
<string name="nwc_payment_request">付款請求已發送,正在等待來自你的錢包的確認</string>
<string name="dvm_waiting_to_confim_payment">正在等待 DVM 確認付款或返回結果</string>
</resources>
+1
View File
@@ -463,6 +463,7 @@
<string name="languages">Languages</string>
<string name="tags">Tags</string>
<string name="posting_policy">Posting policy</string>
<string name="relay_error_messages">Errors and Notices from this Relay</string>
<string name="message_length">Message length</string>
<string name="subscriptions">Subscriptions</string>
<string name="filters">Filters</string>
@@ -77,6 +77,7 @@ fun TranslatableRichTextViewer(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -103,6 +104,7 @@ fun TranslatableRichTextViewer(
tags = tags,
backgroundColor = backgroundColor,
id = id,
callbackUri = callbackUri,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -119,6 +121,7 @@ private fun RenderText(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -139,6 +142,7 @@ private fun RenderText(
tags,
backgroundColor,
id,
callbackUri,
accountViewModel,
nav,
)
@@ -46,6 +46,7 @@ class RichTextParser() {
fullUrl: String,
eventTags: ImmutableListOfLists<String>,
description: String?,
callbackUri: String? = null,
): MediaUrlContent? {
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
@@ -59,6 +60,7 @@ class RichTextParser() {
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
contentWarning = frags["content-warning"] ?: tags["content-warning"],
uri = callbackUri,
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
val frags = Nip54InlineMetadata().parse(fullUrl)
@@ -70,6 +72,7 @@ class RichTextParser() {
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
contentWarning = frags["content-warning"] ?: tags["content-warning"],
uri = callbackUri,
)
} else {
null
@@ -103,11 +106,12 @@ class RichTextParser() {
fun parseText(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String?,
): RichTextViewerState {
val urlSet = parseValidUrls(content)
val imagesForPager =
urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags, content) }.associateBy { it.url }
urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags, content, callbackUri) }.associateBy { it.url }
val imageList = imagesForPager.values.toList()
val emojiMap = Nip30CustomEmoji.createEmojiMap(tags)
+2 -2
View File
@@ -3,7 +3,7 @@ accompanistAdaptive = "0.34.0"
activityCompose = "1.9.0"
agp = "8.4.1"
androidKotlinGeohash = "1.0"
androidxJunit = "1.2.0-beta01"
androidxJunit = "1.2.0-rc01"
appcompat = "1.7.0"
audiowaveform = "1.1.1"
benchmark = "1.2.4"
@@ -14,7 +14,7 @@ coil = "2.6.0"
composeBom = "2024.05.00"
coreKtx = "1.13.1"
espressoCore = "3.5.1"
firebaseBom = "33.0.0"
firebaseBom = "33.1.0"
fragmentKtx = "1.7.1"
gms = "4.4.1"
jacksonModuleKotlin = "2.17.1"
@@ -38,9 +38,12 @@ class NIP90ContentDiscoveryRequestEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 5300
const val ALT = "NIP90 Content Discovery request"
fun create(
dvmPublicKey: String,
dvmPublicKey: HexKey,
forUser: HexKey,
relays: Set<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit,
@@ -48,8 +51,10 @@ class NIP90ContentDiscoveryRequestEvent(
val content = ""
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("p", dvmPublicKey))
tags.add(arrayOf("alt", "NIP90 Content Discovery request"))
tags.add(arrayOf("client", "Amethyst"))
tags.add(arrayOf("alt", ALT))
tags.add(arrayOf("relays") + relays.toTypedArray())
tags.add(arrayOf("param", "max_results", "200"))
tags.add(arrayOf("param", "user", forUser))
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
}