mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
Merge conflict-resolved changes from upstream
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
/.idea/artifacts/*
|
||||
/.idea/kotlinNotebook.xml
|
||||
/.idea/ChatHistory_schema_v3.xml
|
||||
/.idea/markdown.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
+6673
File diff suppressed because it is too large
Load Diff
@@ -44,9 +44,9 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode = 428
|
||||
versionName = generateVersionName("1.03.0")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\""
|
||||
versionCode = 430
|
||||
versionName = generateVersionName("1.04.2")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"3a03c75d85aaf6b181d3b232d064c4d4feea5c73f0bea2bd91ed61b8da7cd6a6\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -151,7 +151,7 @@ android {
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: remove this when lightcompressor uses one MP4 parser only
|
||||
packaging {
|
||||
resources {
|
||||
|
||||
@@ -110,7 +110,7 @@ class ImageUploadTesting {
|
||||
.upload(
|
||||
inputStream = inputStream,
|
||||
hash = initialHash,
|
||||
length = paylod.size,
|
||||
length = paylod.size.toLong(),
|
||||
baseFileName = "filename.png",
|
||||
contentType = "image/png",
|
||||
alt = null,
|
||||
|
||||
@@ -133,6 +133,42 @@
|
||||
<data android:host="njump.me" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="njump.to">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="njump.to" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="iris.to">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="iris.to" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="zap.stream">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="zap.stream" />
|
||||
<data android:pathPrefix="/p/" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="shosho.live">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="shosho.live" />
|
||||
<data android:pathPrefix="/profile/" />
|
||||
<data android:pathPrefix="/live/" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Primal">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -58,17 +58,6 @@ fun debugState(context: Context) {
|
||||
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
|
||||
}
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Connected Relays: " +
|
||||
Amethyst.instance.client
|
||||
.relayStatusFlow()
|
||||
.value.connected.size + "/" +
|
||||
Amethyst.instance.client
|
||||
.relayStatusFlow()
|
||||
.value.available.size,
|
||||
)
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Indexed Relays: " +
|
||||
|
||||
@@ -77,9 +77,9 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
|
||||
@Serializable
|
||||
data class AccountInfo(
|
||||
val npub: String,
|
||||
val hasPrivKey: Boolean,
|
||||
val loggedInWithExternalSigner: Boolean,
|
||||
val isTransient: Boolean,
|
||||
val hasPrivKey: Boolean = false,
|
||||
val loggedInWithExternalSigner: Boolean = false,
|
||||
val isTransient: Boolean = false,
|
||||
)
|
||||
|
||||
private object PrefKeys {
|
||||
|
||||
@@ -706,7 +706,7 @@ class Account(
|
||||
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
|
||||
if (event is MetadataEvent || event is AdvertisedRelayListEvent) {
|
||||
// everywhere
|
||||
return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available
|
||||
return followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value
|
||||
}
|
||||
if (event is GiftWrapEvent) {
|
||||
val receiver = event.recipientPubKey()
|
||||
@@ -906,7 +906,7 @@ class Account(
|
||||
}
|
||||
|
||||
fun sendLiterallyEverywhere(event: Event) {
|
||||
client.send(event, followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available)
|
||||
client.send(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -89,9 +89,9 @@ class HashtagListState(
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
return if (hashtagList == null) {
|
||||
HashtagListEvent.create(hashtag, true, signer)
|
||||
HashtagListEvent.create(hashtag.lowercase(), true, signer)
|
||||
} else {
|
||||
HashtagListEvent.add(hashtagList, hashtag, true, signer)
|
||||
HashtagListEvent.add(hashtagList, hashtag.lowercase(), true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ fun GetVideoController(
|
||||
if (controllerId.needsController()) {
|
||||
// If there is a connection, don't wait.
|
||||
if (!onlyOnePreparing.getAndSet(true)) {
|
||||
scope.launch {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}")
|
||||
PlaybackServiceClient.prepareController(
|
||||
mediaControllerState = controllerId,
|
||||
@@ -172,7 +172,7 @@ fun GetVideoController(
|
||||
// if the controller is not null, just continue playing what the controller was playing
|
||||
if (controllerId.needsController()) {
|
||||
if (!onlyOnePreparing.getAndSet(true)) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("PlaybackService", "Preparing Video from Resume ${controllerId.id} ${mediaItem.src.videoUri} ")
|
||||
PlaybackServiceClient.prepareController(
|
||||
mediaControllerState = controllerId,
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ class AccountFollowsLoaderSubAssembler(
|
||||
|
||||
println("AccountFollowNeeds ${users.size}")
|
||||
|
||||
val connectedRelays = client.relayStatusFlow().value.connected
|
||||
val connectedRelays = client.connectedRelaysFlow().value
|
||||
|
||||
val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried)
|
||||
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ class UserOutboxFinderSubAssembler(
|
||||
if (noOutboxList.isEmpty()) return null
|
||||
|
||||
val accounts = keys.mapTo(mutableSetOf()) { it.account }
|
||||
val connectedRelays = client.relayStatusFlow().value.connected
|
||||
val connectedRelays = client.connectedRelaysFlow().value
|
||||
|
||||
val perRelayKeysBoth =
|
||||
pickRelaysToLoadUsers(
|
||||
|
||||
@@ -141,7 +141,7 @@ fun uriToRoute(
|
||||
return Route.Notification
|
||||
}
|
||||
if (isHashtagRoute(uri)) {
|
||||
return Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id="))
|
||||
return Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id=").lowercase())
|
||||
}
|
||||
|
||||
val nip19 = Nip19Parser.uriToRoute(uri)?.entity
|
||||
|
||||
+30
-25
@@ -50,12 +50,14 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
@@ -64,11 +66,12 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Configuration for zap button behavior and appearance
|
||||
@@ -91,6 +94,7 @@ data class ZapButtonCallbacks(
|
||||
val onZapComplete: ((Boolean) -> Unit)? = null,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
fun ReusableZapButton(
|
||||
baseNote: Note,
|
||||
@@ -100,9 +104,6 @@ fun ReusableZapButton(
|
||||
callbacks: ZapButtonCallbacks = ZapButtonCallbacks(),
|
||||
) {
|
||||
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
|
||||
var wantsToPay by remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(persistentListOf())
|
||||
}
|
||||
|
||||
// Makes sure the user is loaded to get his ln address ahead of time (for DVM buttons)
|
||||
if (config.showUserFinderSubscription) {
|
||||
@@ -138,7 +139,19 @@ fun ReusableZapButton(
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser)
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) {
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(it, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -166,25 +179,17 @@ fun ReusableZapButton(
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) { zappingProgress = it }
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(
|
||||
payingInvoices = wantsToPay,
|
||||
accountViewModel = accountViewModel,
|
||||
onClose = { wantsToPay = persistentListOf() },
|
||||
onError = {
|
||||
wantsToPay = persistentListOf()
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
|
||||
}
|
||||
},
|
||||
justShowError = {
|
||||
scope.launch {
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) {
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(it, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -692,7 +692,7 @@ fun HashTag(
|
||||
modifier =
|
||||
remember {
|
||||
Modifier.clickable {
|
||||
nav.nav(Route.Hashtag(segment.hashtag))
|
||||
nav.nav(Route.Hashtag(segment.hashtag.lowercase()))
|
||||
}
|
||||
},
|
||||
inlineContent =
|
||||
|
||||
@@ -67,14 +67,14 @@ class FilterByListParams(
|
||||
|
||||
fun match(
|
||||
noteEvent: Event,
|
||||
comingFrom: List<NormalizedRelayUrl> = emptyList(),
|
||||
comingFrom: List<NormalizedRelayUrl>,
|
||||
) = ((isGlobal(comingFrom)) || isEventInList(noteEvent)) &&
|
||||
(isHiddenList || isNotHidden(noteEvent.pubKey)) &&
|
||||
isNotInTheFuture(noteEvent)
|
||||
|
||||
fun match(
|
||||
address: Address?,
|
||||
comingFrom: List<NormalizedRelayUrl> = emptyList(),
|
||||
comingFrom: List<NormalizedRelayUrl>,
|
||||
) = address != null &&
|
||||
(isGlobal(comingFrom) || isAuthorInFollows(address)) &&
|
||||
(isHiddenList || isNotHidden(address.pubKeyHex))
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute
|
||||
import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||
@@ -91,6 +92,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
||||
@@ -142,6 +144,10 @@ fun AppNavigation(
|
||||
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.BookmarkGroupView> { BookmarkGroupScreen(it.dTag, it.bookmarkType, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.QRDisplay> { ShowQRScreen(it.pubkey, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.ManualZapSplitPayment> { PayViaIntentScreen(it.paymentId, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||
|
||||
|
||||
+46
-76
@@ -106,18 +106,17 @@ import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font18SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.IconRowModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Width16Space
|
||||
import com.vitorpamplona.amethyst.ui.theme.bannerModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.drawerSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -125,7 +124,8 @@ import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@Composable
|
||||
fun DrawerContent(
|
||||
@@ -546,40 +546,6 @@ fun ListContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayStatus(accountViewModel: AccountViewModel) {
|
||||
val connectedRelaysText by accountViewModel.account.client
|
||||
.relayStatusFlow()
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
RenderRelayStatus(connectedRelaysText)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRelayStatus(relayPool: RelayPool.RelayPoolStatus) {
|
||||
val text by
|
||||
remember(relayPool) { derivedStateOf { "${relayPool.connected.size}/${relayPool.available.size}" } }
|
||||
|
||||
val placeHolder = MaterialTheme.colorScheme.placeholderText
|
||||
|
||||
val color by
|
||||
remember(relayPool) {
|
||||
derivedStateOf {
|
||||
if (relayPool.isConnected) {
|
||||
placeHolder
|
||||
} else {
|
||||
Color.Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationRow(
|
||||
title: Int,
|
||||
@@ -700,44 +666,61 @@ fun IconRowRelays(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp)
|
||||
.clickable(onClick = onClick),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.relays, 4),
|
||||
contentDescription = stringRes(R.string.relay_setup),
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.relays, 4),
|
||||
contentDescription = stringRes(R.string.relay_setup),
|
||||
modifier = Size22Modifier,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = stringRes(id = R.string.relay_setup),
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
Text(
|
||||
modifier = IconRowTextModifier,
|
||||
text = stringRes(id = R.string.relay_setup),
|
||||
fontSize = Font18SP,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(Size16dp))
|
||||
Spacer(modifier = Width16Space)
|
||||
|
||||
RelayStatus(accountViewModel = accountViewModel)
|
||||
}
|
||||
RelayStatus(accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
class PoolStatus(
|
||||
val share: String,
|
||||
val isConnected: Boolean,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun RelayStatus(accountViewModel: AccountViewModel) {
|
||||
val statusCounterFlow: Flow<PoolStatus> =
|
||||
remember(accountViewModel) {
|
||||
combine(
|
||||
accountViewModel.account.client.connectedRelaysFlow(),
|
||||
accountViewModel.account.client.availableRelaysFlow(),
|
||||
) { connected, available ->
|
||||
PoolStatus("${connected.size}/${available.size}", connected.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
val relayPool by statusCounterFlow.collectAsStateWithLifecycle(PoolStatus("", false))
|
||||
|
||||
Text(
|
||||
text = relayPool.share,
|
||||
color = if (relayPool.isConnected) MaterialTheme.colorScheme.placeholderText else Color.Red,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomContent(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
// store the dialog open or close state
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 15.dp),
|
||||
@@ -781,7 +764,7 @@ fun BottomContent(
|
||||
Box(modifier = Modifier.weight(1F))
|
||||
IconButton(
|
||||
onClick = {
|
||||
dialogOpen = true
|
||||
nav.nav(Route.QRDisplay(user.pubkeyHex))
|
||||
nav.closeDrawer()
|
||||
},
|
||||
) {
|
||||
@@ -794,17 +777,4 @@ fun BottomContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogOpen) {
|
||||
ShowQRDialog(
|
||||
user,
|
||||
accountViewModel,
|
||||
onScan = {
|
||||
dialogOpen = false
|
||||
nav.closeDrawer()
|
||||
nav.nav(it)
|
||||
},
|
||||
onClose = { dialogOpen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ sealed class Route {
|
||||
val id: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class QRDisplay(
|
||||
val pubkey: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class ContentDiscovery(
|
||||
val id: String,
|
||||
) : Route()
|
||||
@@ -260,6 +264,11 @@ sealed class Route {
|
||||
val version: String? = null,
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class ManualZapSplitPayment(
|
||||
val paymentId: String,
|
||||
) : Route()
|
||||
}
|
||||
|
||||
inline fun <reified T : Route> isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute<T>() == true
|
||||
@@ -290,6 +299,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||
dest.hasRoute<Route.Geohash>() -> entry.toRoute<Route.Geohash>()
|
||||
dest.hasRoute<Route.Community>() -> entry.toRoute<Route.Community>()
|
||||
|
||||
dest.hasRoute<Route.QRDisplay>() -> entry.toRoute<Route.QRDisplay>()
|
||||
dest.hasRoute<Route.RelayInfo>() -> entry.toRoute<Route.RelayInfo>()
|
||||
|
||||
dest.hasRoute<Route.RoomByAuthor>() -> entry.toRoute<Route.RoomByAuthor>()
|
||||
@@ -318,6 +328,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||
dest.hasRoute<Route.PeopleListManagement>() -> entry.toRoute<Route.PeopleListManagement>()
|
||||
dest.hasRoute<Route.NewGroupDM>() -> entry.toRoute<Route.NewGroupDM>()
|
||||
dest.hasRoute<Route.UserSettings>() -> entry.toRoute<Route.UserSettings>()
|
||||
dest.hasRoute<Route.ManualZapSplitPayment>() -> entry.toRoute<Route.ManualZapSplitPayment>()
|
||||
|
||||
else -> {
|
||||
null
|
||||
|
||||
+17
-25
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -122,19 +121,17 @@ fun nip05VerificationAsAState(
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(
|
||||
baseNote: Note,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchAuthor(baseNote = baseNote, accountViewModel) {
|
||||
ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav)
|
||||
ObserveDisplayNip05Status(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(
|
||||
baseUser: User,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -143,14 +140,12 @@ fun ObserveDisplayNip05Status(
|
||||
|
||||
CrossfadeIfEnabled(
|
||||
targetState = nip05,
|
||||
modifier = columnModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
VerifyAndDisplayNIP05OrStatusLine(
|
||||
it,
|
||||
statuses,
|
||||
baseUser,
|
||||
columnModifier,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
@@ -162,29 +157,26 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
nip05: String?,
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
baseUser: User,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Column(modifier = columnModifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel)
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else if (!statuses.isEmpty()) {
|
||||
ObserveRotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
}
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else if (!statuses.isEmpty()) {
|
||||
ObserveRotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
if (!statuses.isEmpty()) {
|
||||
RotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayUsersNpub(baseUser.pubkeyDisplayHex())
|
||||
}
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
if (!statuses.isEmpty()) {
|
||||
RotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayUsersNpub(baseUser.pubkeyDisplayHex())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +245,7 @@ fun DisplayStatus(
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by observeNote(addressableNote, accountViewModel)
|
||||
val noteEvent = noteState?.note?.event as? StatusEvent ?: return
|
||||
val noteEvent = noteState.note.event as? StatusEvent ?: return
|
||||
|
||||
DisplayStatus(noteEvent, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -1073,10 +1073,12 @@ fun SecondUserInfoRow(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = UserNameMaxRowHeight,
|
||||
) {
|
||||
if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) {
|
||||
ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
|
||||
} else {
|
||||
ObserveDisplayNip05Status(noteAuthor, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
|
||||
Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) {
|
||||
if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) {
|
||||
ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
|
||||
} else {
|
||||
ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
val geo = remember(noteEvent) { noteEvent.geoHashOrScope() }
|
||||
|
||||
@@ -91,10 +91,9 @@ import com.vitorpamplona.amethyst.ui.theme.QuickActionPopupShadow
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallestBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private fun lightenColor(
|
||||
@@ -123,17 +122,13 @@ val externalLinkForNote = { note: Note ->
|
||||
"https://nostrbounties.com/b/${note.toNAddr()}"
|
||||
} else if (note.event is PeopleListEvent) {
|
||||
"https://listr.lol/a/${note.toNAddr()}"
|
||||
} else if (note.event is AudioTrackEvent) {
|
||||
"https://zapstr.live/?track=${note.toNAddr()}"
|
||||
} else if (note.event is FollowListEvent) {
|
||||
"https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}"
|
||||
} else {
|
||||
njumpLink(note.toNAddr())
|
||||
}
|
||||
} else {
|
||||
if (note.event is FileHeaderEvent) {
|
||||
"https://filestr.vercel.app/e/${note.toNEvent()}"
|
||||
} else {
|
||||
njumpLink(note.toNEvent())
|
||||
}
|
||||
njumpLink(note.toNEvent())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -81,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
@@ -102,14 +102,14 @@ import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -499,7 +499,7 @@ private fun RenderOptionBeforeVote(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalUuidApi::class)
|
||||
fun ZapVote(
|
||||
baseNote: Note,
|
||||
poolOption: PollOption,
|
||||
@@ -513,14 +513,8 @@ fun ZapVote(
|
||||
val isLoggedUser by remember { derivedStateOf { accountViewModel.isLoggedUser(baseNote.author) } }
|
||||
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToPay by remember {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var zapStartingTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) }
|
||||
|
||||
@@ -563,7 +557,6 @@ fun ZapVote(
|
||||
accountViewModel.zapAmountChoices().size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
|
||||
) {
|
||||
zapStartingTime = TimeUtils.now()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.zapAmountChoices().first() * 1000,
|
||||
@@ -584,12 +577,13 @@ fun ZapVote(
|
||||
),
|
||||
) {
|
||||
if (wantsToZap) {
|
||||
val context = LocalContext.current
|
||||
FilteredZapAmountChoicePopup(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
pollViewModel,
|
||||
poolOption.option,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onZapStarts = { },
|
||||
onDismiss = {
|
||||
wantsToZap = false
|
||||
zappingProgress = 0f
|
||||
@@ -600,33 +594,17 @@ fun ZapVote(
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(
|
||||
payingInvoices = wantsToPay,
|
||||
accountViewModel = accountViewModel,
|
||||
onClose = { wantsToPay = persistentListOf() },
|
||||
onError = {
|
||||
wantsToPay = persistentListOf()
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog =
|
||||
StringToastMsg(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
it.error,
|
||||
)
|
||||
}
|
||||
},
|
||||
justShowError = {
|
||||
scope.launch {
|
||||
showErrorMessageDialog =
|
||||
StringToastMsg(
|
||||
stringRes(context, R.string.error_dialog_zap_error),
|
||||
it.error,
|
||||
)
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) { error ->
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = StringToastMsg(stringRes(context, R.string.error_dialog_zap_error), error)
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo
|
||||
@@ -169,6 +170,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Composable
|
||||
fun ReactionsRow(
|
||||
@@ -970,7 +973,7 @@ private fun likeClick(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalUuidApi::class)
|
||||
fun ZapReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
@@ -983,12 +986,6 @@ fun ZapReaction(
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||
var wantsToPay by
|
||||
remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -1022,7 +1019,19 @@ fun ZapReaction(
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) { error ->
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -1053,7 +1062,19 @@ fun ZapReaction(
|
||||
}
|
||||
},
|
||||
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) { error ->
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1064,26 +1085,6 @@ fun ZapReaction(
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(
|
||||
payingInvoices = wantsToPay,
|
||||
accountViewModel = accountViewModel,
|
||||
onClose = { wantsToPay = persistentListOf() },
|
||||
onError = {
|
||||
wantsToPay = persistentListOf()
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
|
||||
}
|
||||
},
|
||||
justShowError = {
|
||||
scope.launch {
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToSetCustomZap) {
|
||||
ZapCustomDialog(
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
@@ -1095,7 +1096,19 @@ fun ZapReaction(
|
||||
}
|
||||
},
|
||||
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
onPayViaIntent = {
|
||||
if (it.size == 1) {
|
||||
val payable = it.first()
|
||||
payViaIntent(payable.invoice, context, { }) { error ->
|
||||
zappingProgress = 0f
|
||||
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
val uid = Uuid.random().toString()
|
||||
accountViewModel.tempManualPaymentCache.put(uid, it)
|
||||
nav.nav(Route.ManualZapSplitPayment(uid))
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
baseNote = baseNote,
|
||||
)
|
||||
|
||||
@@ -353,7 +353,7 @@ fun UpdateZapAmountContent(
|
||||
postViewModel.copyFromClipboard(it)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
accountViewModel.toastManager.toast(R.string.invalid_nip47_uri_title, R.string.invalid_nip47_uri_description)
|
||||
accountViewModel.toastManager.toast(R.string.invalid_nip47_uri_title, R.string.invalid_nip47_uri_description, uri ?: "")
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -26,15 +26,18 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -42,6 +45,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -64,16 +68,16 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -281,104 +285,115 @@ fun ZapButton(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PayViaIntentDialog(
|
||||
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
|
||||
fun PayViaIntentScreen(
|
||||
paymentId: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit,
|
||||
onError: (UserBasedErrorMessage) -> Unit,
|
||||
justShowError: (UserBasedErrorMessage) -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(stringRes(id = R.string.manual_zaps), nav::popBack)
|
||||
},
|
||||
) { pad ->
|
||||
val list = accountViewModel.tempManualPaymentCache.get(paymentId)
|
||||
|
||||
if (payingInvoices.size == 1) {
|
||||
val payable = payingInvoices.first()
|
||||
payViaIntent(payable.invoice, context, onClose) {
|
||||
onError(UserBasedErrorMessage(it, payable.info.user))
|
||||
}
|
||||
} else {
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties =
|
||||
DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false,
|
||||
if (list == null) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(stringRes(R.string.feed_is_empty))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
Modifier.padding(
|
||||
16.dp,
|
||||
pad.calculateTopPadding(),
|
||||
16.dp,
|
||||
pad.calculateBottomPadding(),
|
||||
),
|
||||
) {
|
||||
SetDialogToEdgeToEdge()
|
||||
Surface {
|
||||
Column(modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton(onPress = onClose)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
payingInvoices.forEachIndexed { index, payable ->
|
||||
val paid = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
if (payable.info.user != null) {
|
||||
BaseUserPicture(payable.info.user, Size55dp, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (payable.info.user != null) {
|
||||
UsernameDisplay(payable.info.user, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = stringRes(id = R.string.wallet_number, index + 1),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Text(
|
||||
text = stringRes(id = R.string.sats),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
PayButton(isActive = !paid.value) {
|
||||
payViaIntent(payable.invoice, context, { paid.value = true }) {
|
||||
justShowError(UserBasedErrorMessage(it, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
itemsIndexed(
|
||||
accountViewModel.tempManualPaymentCache.get(paymentId) ?: emptyList(),
|
||||
key = { _: Int, invoice: ZapPaymentHandler.Payable ->
|
||||
invoice.invoice
|
||||
},
|
||||
) { index, payable ->
|
||||
DisplayPayable(index, payable, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayPayable(
|
||||
index: Int,
|
||||
payable: ZapPaymentHandler.Payable,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val paid = rememberSaveable(payable) { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
if (payable.info.user != null) {
|
||||
BaseUserPicture(payable.info.user, Size55dp, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (payable.info.user != null) {
|
||||
UsernameDisplay(payable.info.user, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = stringRes(id = R.string.wallet_number, index + 1),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Text(
|
||||
text = stringRes(id = R.string.sats),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
val context = LocalContext.current
|
||||
|
||||
PayButton(isActive = !paid.value) {
|
||||
payViaIntent(payable.invoice, context, { paid.value = true }) {
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.error_dialog_zap_error,
|
||||
UserBasedErrorMessage(it, payable.info.user),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun payViaIntent(
|
||||
invoice: String,
|
||||
context: Context,
|
||||
|
||||
+1
-1
@@ -86,6 +86,6 @@ private fun DisplayTagList(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
) {
|
||||
nav.nav(Route.Hashtag(firstTag))
|
||||
nav.nav(Route.Hashtag(firstTag.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ fun DisplayUncitedHashtags(
|
||||
unusedHashtags.forEach { hashtag ->
|
||||
ClickableTextColor(
|
||||
text = "#$hashtag ",
|
||||
onClick = { nav.nav(Route.Hashtag(hashtag)) },
|
||||
onClick = { nav.nav(Route.Hashtag(hashtag.lowercase())) },
|
||||
linkColor = MaterialTheme.colorScheme.lessImportantLink,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ fun DisplayHashtagExternalId(
|
||||
nav: INav,
|
||||
) {
|
||||
DisplayHashtagExternalId(externalId.topic) {
|
||||
nav.nav(Route.Hashtag(externalId.topic))
|
||||
nav.nav(Route.Hashtag(externalId.topic.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -175,6 +175,8 @@ class AccountViewModel(
|
||||
val toastManager = ToastManager()
|
||||
val feedStates = AccountFeedContentStates(account, viewModelScope)
|
||||
|
||||
val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val notificationHasNewItems =
|
||||
combineTransform(
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ open class DiscoverLongFormFeedFilter(
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet(LongTextNoteEvent.KIND) { _, it ->
|
||||
val noteEvent = it.event
|
||||
noteEvent is LongTextNoteEvent && params.match(noteEvent)
|
||||
noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
return sort(notes)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ open class DiscoverLongFormFeedFilter(
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
noteEvent is LongTextNoteEvent && params.match(noteEvent)
|
||||
noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -51,7 +51,7 @@ open class DiscoverChatFeedFilter(
|
||||
val note = LocalCache.getNoteIfExists(channel.idHex)
|
||||
val noteEvent = note?.event
|
||||
|
||||
if (noteEvent == null || params.match(noteEvent)) {
|
||||
if (noteEvent == null || params.match(noteEvent, note.relays)) {
|
||||
note
|
||||
} else {
|
||||
null
|
||||
@@ -75,7 +75,7 @@ open class DiscoverChatFeedFilter(
|
||||
return collection.mapNotNullTo(HashSet()) { note ->
|
||||
// note event here will never be null
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) {
|
||||
if (noteEvent is ChannelCreateEvent && params.match(noteEvent, note.relays)) {
|
||||
if ((LocalCache.getPublicChatChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) {
|
||||
note
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ open class DiscoverChatFeedFilter(
|
||||
val channelEvent = channel?.event
|
||||
|
||||
if (channel != null &&
|
||||
(channelEvent == null || (channelEvent is ChannelCreateEvent && params.match(channelEvent)))
|
||||
(channelEvent == null || (channelEvent is ChannelCreateEvent && params.match(channelEvent, note.relays)))
|
||||
) {
|
||||
if ((LocalCache.getPublicChatChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) {
|
||||
channel
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ open class DiscoverFollowSetsFeedFilter(
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet(FollowListEvent.KIND) { _, it ->
|
||||
val noteEvent = it.event
|
||||
noteEvent is FollowListEvent && params.match(noteEvent)
|
||||
noteEvent is FollowListEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
|
||||
return sort(notes)
|
||||
@@ -67,7 +67,7 @@ open class DiscoverFollowSetsFeedFilter(
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
noteEvent is FollowListEvent && params.match(noteEvent)
|
||||
noteEvent is FollowListEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -174,8 +174,8 @@ fun RenderFollowSetThumb(
|
||||
it,
|
||||
),
|
||||
contentScale = ContentScale.Crop,
|
||||
mainImageModifier = Modifier,
|
||||
loadedImageModifier = FollowSetImageModifier,
|
||||
mainImageModifier = FollowSetImageModifier,
|
||||
loadedImageModifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
||||
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ open class DiscoverLiveFeedFilter(
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
noteEvent is LiveActivitiesEvent && filterParams.match(noteEvent)
|
||||
noteEvent is LiveActivitiesEvent && filterParams.match(noteEvent, it.relays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ open class DiscoverCommunityFeedFilter(
|
||||
if (noteEvent == null && shouldInclude(key, filterParams, note.relays)) {
|
||||
// send unloaded communities to the screen
|
||||
note
|
||||
} else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) {
|
||||
} else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent, note.relays)) {
|
||||
note
|
||||
} else {
|
||||
null
|
||||
@@ -84,7 +84,7 @@ open class DiscoverCommunityFeedFilter(
|
||||
.mapNotNull { note ->
|
||||
// note event here will never be null
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) {
|
||||
if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent, note.relays)) {
|
||||
listOf(note)
|
||||
} else if (noteEvent is CommunityPostApprovalEvent) {
|
||||
noteEvent.communityAddresses().mapNotNull {
|
||||
@@ -93,7 +93,7 @@ open class DiscoverCommunityFeedFilter(
|
||||
|
||||
if (definitionEvent == null && shouldInclude(it, filterParams, definitionNote.relays)) {
|
||||
definitionNote
|
||||
} else if (definitionEvent is CommunityDefinitionEvent && filterParams.match(definitionEvent)) {
|
||||
} else if (definitionEvent is CommunityDefinitionEvent && filterParams.match(definitionEvent, definitionNote.relays)) {
|
||||
definitionNote
|
||||
} else {
|
||||
null
|
||||
|
||||
+7
-3
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
@@ -74,16 +75,19 @@ open class DiscoverNIP89FeedFilter(
|
||||
fun acceptDVM(note: Note): Boolean {
|
||||
val noteEvent = note.event
|
||||
return if (noteEvent is AppDefinitionEvent) {
|
||||
acceptDVM(noteEvent)
|
||||
acceptDVM(noteEvent, note.relays)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptDVM(noteEvent: AppDefinitionEvent): Boolean {
|
||||
fun acceptDVM(
|
||||
noteEvent: AppDefinitionEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
): Boolean {
|
||||
val filterParams = buildFilterParams(account)
|
||||
return noteEvent.appMetaData()?.subscription != true &&
|
||||
filterParams.match(noteEvent) &&
|
||||
filterParams.match(noteEvent, relays) &&
|
||||
noteEvent.includeKind(5300) &&
|
||||
noteEvent.createdAt > lastAnnounced // && params.match(noteEvent)
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ open class DiscoverMarketplaceFeedFilter(
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet(ClassifiedsEvent.KIND) { _, it ->
|
||||
val noteEvent = it.event
|
||||
noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent)
|
||||
noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent, it.relays)
|
||||
}
|
||||
|
||||
return sort(notes)
|
||||
@@ -68,7 +68,7 @@ open class DiscoverMarketplaceFeedFilter(
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent)
|
||||
noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent, it.relays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
@@ -96,6 +97,7 @@ class FollowPackFeedConversationsFeedFilter(
|
||||
|
||||
fun acceptableEvent(
|
||||
event: Event?,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
filterParams: FilterByListParams,
|
||||
): Boolean =
|
||||
(
|
||||
@@ -107,12 +109,12 @@ class FollowPackFeedConversationsFeedFilter(
|
||||
event is PublicMessageEvent ||
|
||||
event is LiveActivitiesChatMessageEvent
|
||||
) &&
|
||||
filterParams.match(event)
|
||||
filterParams.match(event, relays)
|
||||
|
||||
fun acceptableEvent(
|
||||
note: Note,
|
||||
filterParams: FilterByListParams,
|
||||
): Boolean = acceptableEvent(note.event, filterParams) && !note.isNewThread()
|
||||
): Boolean = acceptableEvent(note.event, note.relays, filterParams) && !note.isNewThread()
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
|
||||
+4
-2
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
@@ -74,6 +75,7 @@ class HomeConversationsFeedFilter(
|
||||
|
||||
fun acceptableEvent(
|
||||
event: Event?,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
filterParams: FilterByListParams,
|
||||
): Boolean =
|
||||
(
|
||||
@@ -85,12 +87,12 @@ class HomeConversationsFeedFilter(
|
||||
event is PublicMessageEvent ||
|
||||
event is LiveActivitiesChatMessageEvent
|
||||
) &&
|
||||
filterParams.match(event)
|
||||
filterParams.match(event, relays)
|
||||
|
||||
fun acceptableEvent(
|
||||
note: Note,
|
||||
filterParams: FilterByListParams,
|
||||
): Boolean = acceptableEvent(note.event, filterParams) && !note.isNewThread()
|
||||
): Boolean = acceptableEvent(note.event, note.relays, filterParams) && !note.isNewThread()
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
|
||||
+9
-18
@@ -21,28 +21,25 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.PersonRemove
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier
|
||||
@@ -94,22 +91,16 @@ fun PeopleListItem(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
IconButton(
|
||||
OutlinedIconButton(
|
||||
onClick = {
|
||||
onDeleteUser(user)
|
||||
},
|
||||
modifier =
|
||||
HalfPadding
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.errorContainer,
|
||||
shape = RoundedCornerShape(percent = 80),
|
||||
),
|
||||
modifier = HalfPadding,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Filled.PersonRemove,
|
||||
contentDescription = stringRes(R.string.remove_user_from_the_list),
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+75
-73
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -46,9 +45,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
@@ -60,40 +58,46 @@ fun AllPeopleListFeedView(
|
||||
val peopleListFeedState by peopleListModel.listFlow().collectAsStateWithLifecycle()
|
||||
val followPackFeedState by followPackModel.listFlow().collectAsStateWithLifecycle()
|
||||
|
||||
if (peopleListFeedState.isEmpty() && followPackFeedState.isEmpty()) {
|
||||
AllPeopleListFeedEmpty(
|
||||
message = stringRes(R.string.follow_set_empty_feed_msg),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.PeopleListMetadataEdit())
|
||||
},
|
||||
LazyColumn(
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.PeopleListMetadataEdit())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (peopleListFeedState.isEmpty()) {
|
||||
item {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = stringRes(R.string.feed_is_empty),
|
||||
modifier = Modifier.padding(vertical = Size20dp),
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier =
|
||||
@@ -108,34 +112,46 @@ fun AllPeopleListFeedView(
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.FollowPackMetadataEdit())
|
||||
},
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = {
|
||||
nav.nav(Route.FollowPackMetadataEdit())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (followPackFeedState.isEmpty()) {
|
||||
item {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = stringRes(R.string.feed_is_empty),
|
||||
modifier = Modifier.padding(vertical = Size20dp),
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||
PeopleListItem(
|
||||
modifier =
|
||||
@@ -153,17 +169,3 @@ fun AllPeopleListFeedView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = Size40dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(message)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
+69
-60
@@ -23,8 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -48,8 +46,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
@@ -63,44 +61,43 @@ fun FollowListAndPackAndUserView(
|
||||
val followPackFeedState by accountViewModel.account.followLists.uiListFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
if (followSetsState.isEmpty() && followPackFeedState.isEmpty()) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.5f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(text = stringRes(R.string.follow_set_empty_dialog_msg))
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
} else {
|
||||
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.PeopleListMetadataEdit()) },
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_sets_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.PeopleListMetadataEdit()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (followSetsState.isEmpty()) {
|
||||
item {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = stringRes(R.string.feed_is_empty),
|
||||
modifier = Modifier.padding(vertical = Size20dp),
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(followSetsState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||
PeopleListAndUserItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -136,32 +133,44 @@ fun FollowListAndPackAndUserView(
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.FollowPackMetadataEdit()) },
|
||||
}
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.discover_follows_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
NewListButton(
|
||||
onClick = { nav.nav(Route.FollowPackMetadataEdit()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (followPackFeedState.isEmpty()) {
|
||||
item {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = stringRes(R.string.feed_is_empty),
|
||||
modifier = Modifier.padding(vertical = Size20dp),
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||
FollowPackAndUserItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ class UserProfileGalleryFeedFilter(
|
||||
return (
|
||||
(it.event?.pubKey == user.pubkeyHex && (noteEvent is PictureEvent || noteEvent is VideoEvent || (noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasFromEvent())) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||
) &&
|
||||
params.match(noteEvent) &&
|
||||
params.match(noteEvent, it.relays) &&
|
||||
account.isAcceptable(it)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ fun TabFollowedTags(
|
||||
HashtagHeader(
|
||||
tag = hashtag,
|
||||
account = accountViewModel,
|
||||
onClick = { nav.nav(Route.Hashtag(hashtag)) },
|
||||
onClick = { nav.nav(Route.Hashtag(hashtag.lowercase())) },
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
|
||||
+2
-16
@@ -53,13 +53,13 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25Modifier
|
||||
@@ -127,23 +127,9 @@ fun DrawAdditionalInfo(
|
||||
)
|
||||
}
|
||||
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
if (dialogOpen) {
|
||||
ShowQRDialog(
|
||||
user = user,
|
||||
accountViewModel = accountViewModel,
|
||||
onScan = {
|
||||
dialogOpen = false
|
||||
nav.nav(it)
|
||||
},
|
||||
onClose = { dialogOpen = false },
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Size25Modifier,
|
||||
onClick = { dialogOpen = true },
|
||||
onClick = { nav.nav(Route.QRDisplay(user.pubkeyHex)) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_qrcode, 1),
|
||||
|
||||
+2
@@ -43,6 +43,8 @@ fun NIP19QrCodeScanner(
|
||||
try {
|
||||
if (it != null) {
|
||||
onScan(uriToRoute(it, accountViewModel.account))
|
||||
} else {
|
||||
onScan(null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.ui.screen.loggedIn.qrcode
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ShowQRDialogPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
accountViewModel.userProfile().info =
|
||||
UserMetadata().apply {
|
||||
name = "My Name"
|
||||
picture = "Picture"
|
||||
nip05 = null
|
||||
banner = "http://banner.com/test"
|
||||
website = "http://mywebsite.com/test"
|
||||
about = "This is the about me"
|
||||
}
|
||||
|
||||
ShowQRDialog(
|
||||
user = accountViewModel.userProfile(),
|
||||
accountViewModel = accountViewModel,
|
||||
onScan = {},
|
||||
onClose = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackButton(onPress: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onPress,
|
||||
) {
|
||||
ArrowBackIcon(MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShowQRDialog(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
onScan: (Route) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
var presenting by remember { mutableStateOf(true) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
SetDialogToEdgeToEdge()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
Row {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
BackButton(onPress = onClose)
|
||||
}
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 10.dp,
|
||||
end = 10.dp,
|
||||
top = pad.calculateTopPadding(),
|
||||
bottom = pad.calculateBottomPadding(),
|
||||
).consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
if (presenting) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
model = user.profilePicture(),
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = user.info?.bestName() ?: "",
|
||||
tags = user.info?.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
val nip05 = user.nip05()
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp),
|
||||
) {
|
||||
QrCodeDrawer(user.toNostrUri())
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(horizontal = 30.dp)) {
|
||||
FilledTonalButton(
|
||||
onClick = { presenting = false },
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.fillMaxWidth().height(50.dp),
|
||||
) {
|
||||
Text(text = stringRes(R.string.scan_qr))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NIP19QrCodeScanner(accountViewModel) {
|
||||
if (it == null) {
|
||||
presenting = true
|
||||
} else {
|
||||
onScan(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.ui.screen.loggedIn.qrcode
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ShowQRScreenPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
accountViewModel.userProfile().info =
|
||||
UserMetadata().apply {
|
||||
name = "My Name"
|
||||
picture = "Picture"
|
||||
nip05 = null
|
||||
banner = "http://banner.com/test"
|
||||
website = "http://mywebsite.com/test"
|
||||
about = "This is the about me"
|
||||
}
|
||||
|
||||
ShowQRScreen(
|
||||
pubkey = accountViewModel.userProfile().pubkeyHex,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackButton(onPress: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onPress,
|
||||
) {
|
||||
ArrowBackIcon(MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShowQRScreen(
|
||||
pubkey: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LoadUser(pubkey, accountViewModel) { user ->
|
||||
if (user != null) {
|
||||
ShowQRScreen(
|
||||
user = user,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShowQRScreen(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(
|
||||
"",
|
||||
nav::popBack,
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
top = pad.calculateTopPadding(),
|
||||
bottom = pad.calculateBottomPadding(),
|
||||
).consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ShowQRBody(user, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowQRBody(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var presenting by remember { mutableStateOf(true) }
|
||||
if (presenting) {
|
||||
PresentQR(user, accountViewModel) {
|
||||
presenting = false
|
||||
}
|
||||
} else {
|
||||
NIP19QrCodeScanner(accountViewModel) {
|
||||
if (it == null) {
|
||||
presenting = true
|
||||
} else {
|
||||
nav.nav(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PresentQR(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
switchToScan: () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
model = user.profilePicture(),
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = user.info?.bestName() ?: "",
|
||||
tags = user.info?.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
val nip05 = user.nip05()
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp),
|
||||
) {
|
||||
QrCodeDrawer(user.toNostrUri())
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(horizontal = 30.dp)) {
|
||||
FilledTonalButton(
|
||||
onClick = switchToScan,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.fillMaxWidth().height(50.dp),
|
||||
) {
|
||||
Text(text = stringRes(R.string.scan_qr))
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -50,8 +50,8 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||
|
||||
override fun getRelayList(): List<NormalizedRelayUrl> =
|
||||
account.client
|
||||
.relayStatusFlow()
|
||||
.value.available
|
||||
.availableRelaysFlow()
|
||||
.value
|
||||
.sorted()
|
||||
|
||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||
|
||||
+1
-1
@@ -257,7 +257,7 @@ private fun DisplaySearchResults(
|
||||
hashTags,
|
||||
key = { _, item -> "#$item" },
|
||||
) { _, item ->
|
||||
HashtagLine(item) { nav.nav(Route.Hashtag(item)) }
|
||||
HashtagLine(item.lowercase()) { nav.nav(Route.Hashtag(item.lowercase())) }
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
|
||||
+8
-5
@@ -505,12 +505,15 @@ private fun FullBleedNoteCompose(
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(
|
||||
baseNote,
|
||||
Column(
|
||||
remember { Modifier.weight(1f) },
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
) {
|
||||
ObserveDisplayNip05Status(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
val geo = remember { noteEvent.geoHashOrScope() }
|
||||
if (geo != null) {
|
||||
|
||||
+2
-3
@@ -311,7 +311,7 @@ private fun RenderAuthorInformation(
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, bottom = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
NoteAuthorPicture(note, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
@@ -327,10 +327,9 @@ private fun RenderAuthorInformation(
|
||||
VideoUserOptionAction(note, accountViewModel, nav)
|
||||
}
|
||||
if (accountViewModel.settings.isCompleteUIMode()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ObserveDisplayNip05Status(
|
||||
note.author!!,
|
||||
Modifier.weight(1f),
|
||||
accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ class VideoFeedFilter(
|
||||
(noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
|
||||
(noteEvent is PictureEvent && acceptanceEvent(noteEvent))
|
||||
) &&
|
||||
params.match(noteEvent) &&
|
||||
params.match(noteEvent, note.relays) &&
|
||||
(params.isHiddenList || account.isAcceptable(note))
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,8 @@ val drawerSpacing = Modifier.padding(top = Size10dp, start = Size25dp, end = Siz
|
||||
val IconRowTextModifier = Modifier.padding(start = 16.dp)
|
||||
val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp)
|
||||
|
||||
val Width16Space = Modifier.width(Size16dp)
|
||||
|
||||
val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth()
|
||||
|
||||
val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp)
|
||||
|
||||
@@ -104,8 +104,8 @@ private val LightMediumImportantLink = LightColorPalette.primary.copy(alpha = 0.
|
||||
private val DarkGrayText = DarkColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
private val LightGrayText = LightColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
|
||||
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.42f)
|
||||
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.42f)
|
||||
|
||||
private val DarkOnBackgroundColorFilter = ColorFilter.tint(DarkColorPalette.onBackground)
|
||||
private val LightOnBackgroundColorFilter = ColorFilter.tint(LightColorPalette.onBackground)
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
<string name="report_dialog_post_report_btn">Odeslat zprávu</string>
|
||||
<string name="report_dialog_title">Blokovat a nahlásit</string>
|
||||
<string name="block_only">Blokovat</string>
|
||||
<string name="manual_zaps">Manuální zap rozdělení</string>
|
||||
<string name="bookmarks">Záložky</string>
|
||||
<string name="drafts">Koncepty</string>
|
||||
<string name="private_bookmarks">Soukromé záložky</string>
|
||||
@@ -448,6 +449,7 @@
|
||||
<string name="follow_list_global">Globální</string>
|
||||
<string name="follow_list_mute_list">Seznam ztlumení</string>
|
||||
<string name="follow_sets">Seznamy sledování</string>
|
||||
<string name="follow_sets_explainer">Toto jsou follow seznamy určené pro vaše vlastní použití. Můžete sledovat uživatele soukromě nebo veřejně.</string>
|
||||
<string name="labeled_bookmarks">Označené záložky</string>
|
||||
<string name="general_bookmarks">Obecné záložky</string>
|
||||
<string name="follow_set_type_public">Veřejné</string>
|
||||
@@ -480,6 +482,7 @@
|
||||
<string name="follow_set_creation_item_label">Vytvořit nový seznam %1$s s uživatelem</string>
|
||||
<string name="follow_set_creation_item_description">Vytvoří %1$s sadu sledování a přidá do ní %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nový seznam sledování</string>
|
||||
<string name="follow_pack_creation_dialog_title">Nová sada doporučení</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopírovat/Klonovat seznam sledování</string>
|
||||
<string name="follow_set_desc_modify_label">Upravit popis</string>
|
||||
<string name="follow_set_empty_desc_label">Tento seznam nemá žádný popis</string>
|
||||
@@ -492,6 +495,7 @@
|
||||
<string name="follow_set_creation_action_btn_label">Vytvořit seznam</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopírovat/Klonovat seznam</string>
|
||||
<string name="follow_set_rename_btn_label">Přejmenovat seznam</string>
|
||||
<string name="follow_set_edit_list_metadata">Upravit seznam</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Upravit</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Přejmenováváte z </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> na..</string>
|
||||
@@ -630,6 +634,7 @@
|
||||
<string name="followed_tags">Sledované značky</string>
|
||||
<string name="relay_setup">Rele</string>
|
||||
<string name="discover_follows">Balíčky Sledování</string>
|
||||
<string name="discover_follows_explainer">Toto jsou seznamy uživatelů, které doporučujete ostatním. Jsou povoleni pouze veřejní uživatelé.</string>
|
||||
<string name="discover_reads">Přečtené</string>
|
||||
<string name="discover_content_v2">Algoritmy kanálu</string>
|
||||
<string name="discover_marketplace">Trh</string>
|
||||
@@ -1121,4 +1126,17 @@
|
||||
<string name="remove_user_from_the_list">Odebrat uživatele ze seznamu</string>
|
||||
<string name="follow_list_item_label">Balíček Sledování</string>
|
||||
<string name="members">Členové</string>
|
||||
<string name="people_list_title">Metadata seznamu sledování</string>
|
||||
<string name="people_list_explainer">Metadata seznamů sledování mohou vidět všichni na Nostr. Pouze vaši soukromí členové jsou šifrovaní.</string>
|
||||
<string name="follow_pack_title">Metadata sady doporučení</string>
|
||||
<string name="follow_pack_explainer">Metadata sad doporučení mohou vidět všichni na Nostr a často jsou zveřejňována na různých webech jako startovací sady pro nové uživatele.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Upravit sadu doporučení</string>
|
||||
<string name="follow_pack_creation_name_label">Název sady</string>
|
||||
<string name="follow_pack_copy_name_label">Nový název sady</string>
|
||||
<string name="follow_pack_creation_desc_label">Popis sady</string>
|
||||
<string name="follow_pack_copy_desc_label">Nový popis sady</string>
|
||||
<string name="follow_set_broadcast">Broadcast seznamu</string>
|
||||
<string name="follow_pack_broadcast">Broadcast sady doporučení</string>
|
||||
<string name="follow_set_delete">Smazat seznam</string>
|
||||
<string name="follow_pack_delete">Smazat sadu doporučení</string>
|
||||
</resources>
|
||||
|
||||
@@ -351,6 +351,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="report_dialog_post_report_btn">Meldung veröffentlichen</string>
|
||||
<string name="report_dialog_title">Blockieren und melden</string>
|
||||
<string name="block_only">Blockieren</string>
|
||||
<string name="manual_zaps">Manuelle Zap-Splits</string>
|
||||
<string name="bookmarks">Lesezeichen</string>
|
||||
<string name="drafts">Entwürfe</string>
|
||||
<string name="private_bookmarks">Private Lesezeichen</string>
|
||||
@@ -454,6 +455,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="follow_list_global">Weltweit</string>
|
||||
<string name="follow_list_mute_list">Stummliste</string>
|
||||
<string name="follow_sets">Follower-Listen</string>
|
||||
<string name="follow_sets_explainer">Dies sind Follow-Listen für deine eigene Nutzung. Du kannst Nutzer privat oder öffentlich folgen.</string>
|
||||
<string name="labeled_bookmarks">Markierte Lesezeichen</string>
|
||||
<string name="general_bookmarks">Allgemeine Lesezeichen</string>
|
||||
<string name="follow_set_type_public">Öffentlich</string>
|
||||
@@ -486,6 +488,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="follow_set_creation_item_label">Neue %1$s-Liste mit Benutzer erstellen</string>
|
||||
<string name="follow_set_creation_item_description">Erstellt ein %1$s-Folge-Set und fügt %2$s hinzu.</string>
|
||||
<string name="follow_set_creation_dialog_title">Neue Follower-Liste</string>
|
||||
<string name="follow_pack_creation_dialog_title">Neues Empfehlungspaket</string>
|
||||
<string name="follow_set_copy_dialog_title">Follower-Liste kopieren/klonen</string>
|
||||
<string name="follow_set_desc_modify_label">Beschreibung bearbeiten</string>
|
||||
<string name="follow_set_empty_desc_label">Diese Liste hat keine Beschreibung</string>
|
||||
@@ -498,6 +501,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="follow_set_creation_action_btn_label">Liste erstellen</string>
|
||||
<string name="follow_set_copy_action_btn_label">Liste kopieren/klonen</string>
|
||||
<string name="follow_set_rename_btn_label">Liste umbenennen</string>
|
||||
<string name="follow_set_edit_list_metadata">Liste bearbeiten</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Bearbeiten</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Du benennst um von </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> zu..</string>
|
||||
@@ -635,6 +639,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="followed_tags">Gefolgte Tags</string>
|
||||
<string name="relay_setup">Relais</string>
|
||||
<string name="discover_follows">Folge Paketen</string>
|
||||
<string name="discover_follows_explainer">Dies sind Listen von Nutzern, die du anderen empfiehlst. Nur öffentliche Nutzer sind erlaubt.</string>
|
||||
<string name="discover_reads">Lesungen</string>
|
||||
<string name="discover_content_v2">Feedalgorithmen</string>
|
||||
<string name="discover_marketplace">Marktplatz</string>
|
||||
@@ -1126,4 +1131,17 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="remove_user_from_the_list">Benutzer aus der Liste entfernen</string>
|
||||
<string name="follow_list_item_label">Folge Paket</string>
|
||||
<string name="members">Mitglieder</string>
|
||||
<string name="people_list_title">Metadaten der Follow-Liste</string>
|
||||
<string name="people_list_explainer">Metadaten von Follow-Listen können von allen auf Nostr gesehen werden. Nur deine privaten Mitglieder sind verschlüsselt.</string>
|
||||
<string name="follow_pack_title">Metadaten des Empfehlungspakets</string>
|
||||
<string name="follow_pack_explainer">Metadaten von Empfehlungspaketen können von allen auf Nostr gesehen werden und werden häufig auf Websites als Starterkits für neue Nutzer veröffentlicht.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Empfehlungspaket bearbeiten</string>
|
||||
<string name="follow_pack_creation_name_label">Paketname</string>
|
||||
<string name="follow_pack_copy_name_label">Neuer Paketname</string>
|
||||
<string name="follow_pack_creation_desc_label">Paketbeschreibung</string>
|
||||
<string name="follow_pack_copy_desc_label">Neue Paketbeschreibung</string>
|
||||
<string name="follow_set_broadcast">Liste veröffentlichen</string>
|
||||
<string name="follow_pack_broadcast">Empfehlungspaket veröffentlichen</string>
|
||||
<string name="follow_set_delete">Liste löschen</string>
|
||||
<string name="follow_pack_delete">Empfehlungspaket löschen</string>
|
||||
</resources>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Envoyer</string>
|
||||
<string name="save">Sauvegarder</string>
|
||||
<string name="create">Créer</string>
|
||||
<string name="rename">Renommer</string>
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="failed_to_upload_the_image">Échec du téléchargement de l\'image</string>
|
||||
<string name="relay_address">Adresse du relais</string>
|
||||
@@ -124,6 +125,8 @@
|
||||
<string name="public_chat_feed">Flux de chat public</string>
|
||||
<string name="global_feed">Flux mondial</string>
|
||||
<string name="search_feed">Flux de recherche</string>
|
||||
<string name="search_and_add_a_user">Rechercher et ajouter un utilisateur</string>
|
||||
<string name="add_a_user">Ajouter un utilisateur</string>
|
||||
<string name="add_a_relay">Ajouter un relais</string>
|
||||
<string name="profile_name">Nom</string>
|
||||
<string name="display_name">Nom visible du public</string>
|
||||
@@ -344,6 +347,7 @@
|
||||
<string name="report_dialog_post_report_btn">Publier le signalement</string>
|
||||
<string name="report_dialog_title">Bloquer et Signaler</string>
|
||||
<string name="block_only">Bloquer</string>
|
||||
<string name="manual_zaps">Division manuelle de zaps</string>
|
||||
<string name="bookmarks">Favoris</string>
|
||||
<string name="drafts">Brouillons</string>
|
||||
<string name="private_bookmarks">Favoris Privés</string>
|
||||
@@ -445,6 +449,46 @@
|
||||
<string name="follow_list_aroundme">Autour de moi</string>
|
||||
<string name="follow_list_global">Général</string>
|
||||
<string name="follow_list_mute_list">Liste en Sourdine</string>
|
||||
<string name="labeled_bookmarks">Marque-pages étiquetés</string>
|
||||
<string name="general_bookmarks">Marque-pages généraux</string>
|
||||
<string name="follow_set_type_public">Public</string>
|
||||
<string name="follow_set_type_private">Privé</string>
|
||||
<string name="follow_set_type_mixed">Mixte</string>
|
||||
<string name="follow_set_create_btn_label">Nouveau</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s est un membre public</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s est un membre privé</string>
|
||||
<string name="follow_set_public_member_add_label">Ajouter en tant que membre public</string>
|
||||
<string name="follow_set_private_member_add_label">Ajouter en tant que membre privé</string>
|
||||
<string name="follow_set_public_members_header_label2">Membres publics</string>
|
||||
<string name="follow_set_private_members_header_label2">Membres privés</string>
|
||||
<string name="follow_set_public_members_header_label">Profils publics</string>
|
||||
<string name="follow_set_private_members_header_label">Profils privés</string>
|
||||
<string name="follow_set_single_member_label">membre</string>
|
||||
<string name="follow_set_multiple_member_label">membres</string>
|
||||
<string name="follow_set_empty_label">Aucun membre</string>
|
||||
<string name="follow_set_empty_label2">Vide</string>
|
||||
<string name="follow_set_absence_indicator">%1$s n\'est pas dans cette liste</string>
|
||||
<string name="follow_set_absence_indicator2">%1$s n\'est pas un membre</string>
|
||||
<string name="follow_set_man_dialog_title2">Vos listes et %1$s</string>
|
||||
<string name="follow_set_man_dialog_title">Vos listes</string>
|
||||
<string name="follow_set_error_dialog_msg">Un problème est survenu lors de la récupération : %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Créer une nouvelle liste</string>
|
||||
<string name="follow_set_creation_item_label">Nouvelle liste avec %1$s membres</string>
|
||||
<string name="follow_set_desc_modify_label">Modifier la description</string>
|
||||
<string name="follow_set_empty_desc_label">Cette liste n\'a pas de description</string>
|
||||
<string name="follow_set_current_desc_label">Description actuelle :</string>
|
||||
<string name="follow_set_copy_indicator_description">Définissez un nouveau nom/description pour la liste dupliquée ci-dessous.</string>
|
||||
<string name="follow_set_creation_name_label">Nom de la liste</string>
|
||||
<string name="follow_set_copy_name_label">Nom de la nouvelle liste</string>
|
||||
<string name="follow_set_creation_desc_label">Description de la liste (facultatif)</string>
|
||||
<string name="follow_set_copy_desc_label">Description de la nouvelle liste</string>
|
||||
<string name="follow_set_creation_action_btn_label">Créer une liste</string>
|
||||
<string name="follow_set_copy_action_btn_label">Copier/Dupliquer la liste</string>
|
||||
<string name="follow_set_rename_btn_label">Renommer la liste</string>
|
||||
<string name="follow_set_edit_list_metadata">Modifier la liste</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Modifier</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Vous renommez depuis </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> vers..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Le port par défaut est 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Se connecter à travers Tor avec Orbot
|
||||
\n\n1. Installe [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -580,6 +624,7 @@
|
||||
<string name="followed_tags">Tags suivis</string>
|
||||
<string name="relay_setup">Relais</string>
|
||||
<string name="discover_follows">Packs à suivre</string>
|
||||
<string name="discover_follows_explainer">Ce sont des listes d\'utilisateurs que vous recommandez à d\'autres personnes. Seuls les utilisateurs publics sont autorisés.</string>
|
||||
<string name="discover_reads">Lectures</string>
|
||||
<string name="discover_content_v2">Algorithmes de flux</string>
|
||||
<string name="discover_marketplace">Place de Marché</string>
|
||||
@@ -661,6 +706,8 @@
|
||||
<string name="paste_from_clipboard">Coller depuis le presse-papiers</string>
|
||||
<string name="invalid_nip47_uri_title">URI NIP-47 invalide</string>
|
||||
<string name="invalid_nip47_uri_description">L\'URI %1$s n\'est pas une URI de connexion NIP-47 valide.</string>
|
||||
<string name="connect_to_new_nwc_wallet">Se connecter à un nouveau portefeuille NWC</string>
|
||||
<string name="couldnt_find_nwc_wallets">Portefeuille introuvable</string>
|
||||
<string name="language_description">Pour l\'interface de l\'App</string>
|
||||
<string name="theme_description">Sombre, Clair ou thème Système</string>
|
||||
<string name="automatically_load_images_gifs_description">Charger automatiquement les images et les GIFs</string>
|
||||
@@ -832,6 +879,8 @@
|
||||
<string name="media_compression_quality_medium">Moyenne</string>
|
||||
<string name="media_compression_quality_high">Haute</string>
|
||||
<string name="media_compression_quality_uncompressed">Non compressé</string>
|
||||
<string name="video_codec_h265_label">Utiliser le codec H.265/HEVC</string>
|
||||
<string name="video_codec_h265_description">Une meilleure qualité avec des tailles de fichier plus petites, mais tous les appareils ne supportent pas la lecture H.265.</string>
|
||||
<string name="edit_draft">Modifier le brouillon</string>
|
||||
<string name="login_with_qr_code">Se connecter avec un QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
@@ -890,6 +939,15 @@
|
||||
<string name="search_relays_not_found_description">La création d\'une liste de relais spécialement conçue pour la recherche et l\'étiquetage utilisateur améliorera ces résultats.</string>
|
||||
<string name="search_relays_not_found_editing">Insérez de 1 à 3 relais à utiliser lors de la recherche de contenu ou de l\'étiquetage utilisateur. Assurez-vous que les relais que vous avez choisis implémentent le NIP-50</string>
|
||||
<string name="search_relays_not_found_examples">De bonnes options sont:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
|
||||
<string name="outbox_relays_title">Relais d\'envoi</string>
|
||||
<string name="outbox_relays_not_found">Configurez vos relais d\'envoi pour publier</string>
|
||||
<string name="outbox_relays_not_found_description">Créer une liste de relais spécialement conçue pour recevoir votre contenu est crucial pour votre expérience Nostr et la seule façon que vos abonnés puissent vous trouver. </string>
|
||||
<string name="outbox_relays_not_found_editing">Insérez entre 1 et 3 relais qui reçoivent vos messages. Assurez-vous qu\'ils ne nécessitent pas de paiement si vous ne payez pas pour publier</string>
|
||||
<string name="outbox_relays_not_found_examples">De bonnes options sont :\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||
<string name="inbox_relays_not_found">Configurez vos relais de messagerie publique pour recevoir des notifications</string>
|
||||
<string name="inbox_relays_not_found_description">Créer une liste de relais spécialement conçue pour recevoir des notifications est crucial pour votre expérience Nostr. </string>
|
||||
<string name="inbox_relays_not_found_editing">Insérez entre 1 et 3 relais qui reçoivent des notifications pour vous. Assurez-vous qu\'ils n\'exigent pas de paiement ou de web de confiance de la part de l\'expéditeur, sinon vous ne recevrez que des notifications des payeurs ou des utilisateurs à long terme</string>
|
||||
<string name="inbox_relays_not_found_examples">De bonnes options sont :\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||
<string name="dm_upload">Téléversement de MP</string>
|
||||
<string name="relay_settings">Paramètres du relais</string>
|
||||
<string name="public_home_section">Relais publics d\'accueil</string>
|
||||
@@ -1014,6 +1072,9 @@
|
||||
<string name="torrent_download">Télécharger</string>
|
||||
<string name="torrent_failure">Échec de l\'ouverture du fichier</string>
|
||||
<string name="torrent_no_apps">Aucune application torrent installée pour ouvrir et télécharger le fichier.</string>
|
||||
<string name="torrent_no_info">L\'événement ne possède pas assez d\'informations pour construire un lien magnet</string>
|
||||
<string name="my_lists">Mes listes</string>
|
||||
<string name="people_list_label">Utilisateurs</string>
|
||||
<string name="select_list_to_filter">Sélectionnez une liste pour filtrer le fil</string>
|
||||
<string name="temporary_account">Se déconnecter au verrouillage de l\'appareil</string>
|
||||
<string name="private_message">Message privé</string>
|
||||
@@ -1035,4 +1096,20 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Voulez-vous envoyer le rapport d\'erreur récent à Amethyst dans un MP ? Aucune information personnelle ne sera partagée</string>
|
||||
<string name="crashreport_found_send">Envoyer</string>
|
||||
<string name="this_message_will_disappear_in_days">Ce message disparaîtra dans %1$d jours</string>
|
||||
<string name="in_the_list">Déjà dans la liste</string>
|
||||
<string name="private_members">Membres privés</string>
|
||||
<string name="public_members">Membres publics</string>
|
||||
<string name="private_members_count">Membres privés (%1$s)</string>
|
||||
<string name="public_members_count">Membres publics (%1$s)</string>
|
||||
<string name="add_user_to_the_list">Ajouter l\'utilisateur à la liste</string>
|
||||
<string name="remove_user_from_the_list">Supprimer l\'utilisateur de la liste</string>
|
||||
<string name="members">Membres</string>
|
||||
<string name="follow_pack_creation_name_label">Nom du pack</string>
|
||||
<string name="follow_pack_copy_name_label">Nom du nouveau pack</string>
|
||||
<string name="follow_pack_creation_desc_label">Description du pack</string>
|
||||
<string name="follow_pack_copy_desc_label">Description du nouveau pack</string>
|
||||
<string name="follow_set_broadcast">Liste de diffusion</string>
|
||||
<string name="follow_pack_broadcast">Pack de diffusion</string>
|
||||
<string name="follow_set_delete">Supprimer la liste</string>
|
||||
<string name="follow_pack_delete">Supprimer le pack</string>
|
||||
</resources>
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
<string name="report_dialog_post_report_btn">सूचना प्रकाशित करें</string>
|
||||
<string name="report_dialog_title">बाधित करें तथा सूचना दें</string>
|
||||
<string name="block_only">बाधित करें</string>
|
||||
<string name="manual_zaps">मनुष्यकृत ज्साप विभाजन</string>
|
||||
<string name="bookmarks">स्मर्तव्य</string>
|
||||
<string name="drafts">पाण्डुलिपियाँ</string>
|
||||
<string name="private_bookmarks">निजी स्मर्तव्य</string>
|
||||
@@ -449,7 +450,8 @@
|
||||
<string name="follow_list_aroundme">मेरे आसपास</string>
|
||||
<string name="follow_list_global">वैश्विक</string>
|
||||
<string name="follow_list_mute_list">मौन सूची</string>
|
||||
<string name="follow_sets">अनुगम्य सूचियाँ</string>
|
||||
<string name="follow_sets">अनुचरण सूचियाँ</string>
|
||||
<string name="follow_sets_explainer">ये अनुचरण सूचियाँ आपके उपयोग के लिए रूपांकित हैं। आप प्रयोक्ताओं का अनुचरण कर सकते हैं निजी अथवा सार्वजनिक रूप से</string>
|
||||
<string name="labeled_bookmarks">सूचक युक्त स्मर्त्तव्य सूची</string>
|
||||
<string name="general_bookmarks">सामान्य स्मर्त्तव्य सूची</string>
|
||||
<string name="follow_set_type_public">सार्वजनिक</string>
|
||||
@@ -459,9 +461,9 @@
|
||||
\nनवीकरण के लिए नीचे दबाएँ। अथवा एक नया बनाने के लिए जोड घुण्डियाँ टाँकें।
|
||||
</string>
|
||||
<string name="follow_set_create_btn_label">नव्य</string>
|
||||
<string name="follow_set_add_author_from_note_action">लेखक जोडें अनुगम्य सूची में</string>
|
||||
<string name="follow_set_add_author_from_note_action">लेखक जोडें अनुचरण सूची में</string>
|
||||
<string name="follow_set_profile_actions_menu_description">प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ।</string>
|
||||
<string name="follow_set_icon_description">सूची %1$s के लिए चिह्न</string>
|
||||
<string name="follow_set_icon_description">अनुचरण सूची के लिए चिह्न</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s एक सार्वजनिक सदस्य है</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s एक निजी सदस्य है</string>
|
||||
<string name="follow_set_public_member_add_label">सार्वजनिक सदस्य के रूप में जोडें</string>
|
||||
@@ -484,6 +486,7 @@
|
||||
<string name="follow_set_creation_item_label">प्रयोक्ता के साथ नई सूची %1$s बनाएँ</string>
|
||||
<string name="follow_set_creation_item_description">अनुगम्य सूची %1$s बनाता है तथा उससे %2$s जोडता है।</string>
|
||||
<string name="follow_set_creation_dialog_title">नई %1$s सूची</string>
|
||||
<string name="follow_pack_creation_dialog_title">नव्य अनुचरण पोटली</string>
|
||||
<string name="follow_set_copy_dialog_title">अनुचरण सूची की अनुकृति करें</string>
|
||||
<string name="follow_set_desc_modify_label">विवरण का संशोधन करें</string>
|
||||
<string name="follow_set_empty_desc_label">इस सूची का कोई विवरण नहीं</string>
|
||||
@@ -496,6 +499,7 @@
|
||||
<string name="follow_set_creation_action_btn_label">समुच्चय बनाएँ</string>
|
||||
<string name="follow_set_copy_action_btn_label">सूची की अनुकृति</string>
|
||||
<string name="follow_set_rename_btn_label">समुच्चय पुनःनामकरण</string>
|
||||
<string name="follow_set_edit_list_metadata">सूची सम्पादन</string>
|
||||
<string name="follow_set_desc_modify_btn_label">संशोधन करें</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">आप पनःनामकरण कर रहे हैं इस से </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> इस तक..</string>
|
||||
@@ -634,6 +638,7 @@
|
||||
<string name="followed_tags">अनुचरित विषयसूचक</string>
|
||||
<string name="relay_setup">पुनःप्रसारक</string>
|
||||
<string name="discover_follows">अनुचरण पोटलियाँ</string>
|
||||
<string name="discover_follows_explainer">ये प्रयोक्ता सूचियाँ हैं जो आप अन्य लोगों के लिए अनुशंसित करते हैं। केवल सार्वजनिक प्रयोक्ता अनुमत।</string>
|
||||
<string name="discover_reads">पठितव्य</string>
|
||||
<string name="discover_content_v2">सूचनावली विधियाँ</string>
|
||||
<string name="discover_marketplace">पण्यक्षेत्र</string>
|
||||
@@ -1126,4 +1131,17 @@
|
||||
<string name="remove_user_from_the_list">सूची से प्रयोक्ता हटाएँ</string>
|
||||
<string name="follow_list_item_label">अनुचरण पोटली</string>
|
||||
<string name="members">सदस्य सूची</string>
|
||||
<string name="people_list_title">अनुचरण सूची उपतथ्य</string>
|
||||
<string name="people_list_explainer">अनुचरण सूची उपतथ्य नोस्टर में सब के द्वारा देखा जा सकता है। केवल आपके निजी सदस्य रहस्यीकृत हैं।</string>
|
||||
<string name="follow_pack_title">अनुचरण पोटली उपतथ्य</string>
|
||||
<string name="follow_pack_explainer">अनुचरण पोटली उपतथ्य नोस्टर में सब के द्वारा देखा जा सकता है तथा प्रायः अनेक जालस्थानों पर प्रकाशित होते हैं प्रारम्भ सामग्री के रूप में नये प्रयोक्ताओं के लिए।</string>
|
||||
<string name="follow_pack_edit_list_metadata">अनुचरण पोटली सम्पादन</string>
|
||||
<string name="follow_pack_creation_name_label">पोटली नाम</string>
|
||||
<string name="follow_pack_copy_name_label">नव्य पोटली नाम</string>
|
||||
<string name="follow_pack_creation_desc_label">पोटली विवरण</string>
|
||||
<string name="follow_pack_copy_desc_label">नव्य पोटली विवरण</string>
|
||||
<string name="follow_set_broadcast">सूची प्रसारण</string>
|
||||
<string name="follow_pack_broadcast">पोटली प्रसारण</string>
|
||||
<string name="follow_set_delete">सूची मिटाएँ</string>
|
||||
<string name="follow_pack_delete">पोटली मिटाएँ</string>
|
||||
</resources>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<string name="rename">Átnevezés</string>
|
||||
<string name="cancel">Mégse</string>
|
||||
<string name="failed_to_upload_the_image">Nem sikerült feltölteni a képet</string>
|
||||
<string name="relay_address">Atjátszó címe</string>
|
||||
<string name="relay_address">Átjátszó címe</string>
|
||||
<string name="posts">Bejegyzések</string>
|
||||
<string name="bytes">Bájt</string>
|
||||
<string name="errors">Hibák</string>
|
||||
@@ -347,6 +347,7 @@
|
||||
<string name="report_dialog_post_report_btn">Bejelentés közzététele</string>
|
||||
<string name="report_dialog_title">Letiltás és bejelentés</string>
|
||||
<string name="block_only">Letiltás</string>
|
||||
<string name="manual_zaps">Kézi Zap-megosztás</string>
|
||||
<string name="bookmarks">Könyvjelző</string>
|
||||
<string name="drafts">Piszkozatok</string>
|
||||
<string name="private_bookmarks">Privát könyvjelzők</string>
|
||||
@@ -427,7 +428,7 @@
|
||||
<string name="uploading_state_uploading">Feltöltés…</string>
|
||||
<string name="uploading_state_server_processing">Feldolgozás…</string>
|
||||
<string name="uploading_state_downloading">Letöltés…</string>
|
||||
<string name="uploading_state_hashing">Hash-selés…</string>
|
||||
<string name="uploading_state_hashing">Kivonatolás</string>
|
||||
<string name="uploading_state_finished">Kész</string>
|
||||
<string name="uploading_state_error">Hiba</string>
|
||||
<string name="upload_server_relays_nip95">Saját átjátszók (NIP-95)</string>
|
||||
@@ -444,11 +445,13 @@
|
||||
<string name="follow_list_selection">Követési lista</string>
|
||||
<string name="follow_list_kind3follows">Követettek bejegyzései</string>
|
||||
<string name="follow_list_kind3follows_users_only">Összes követett felhasználó</string>
|
||||
<string name="follow_list_kind3_follows_users_only">Alapértelmezett követési lista</string>
|
||||
<string name="follow_list_kind3follows_proxy">Követés proxyn keresztül</string>
|
||||
<string name="follow_list_aroundme">Közelben lévők bejegyzései</string>
|
||||
<string name="follow_list_global">Globális</string>
|
||||
<string name="follow_list_mute_list">Némítottak bejegyzései</string>
|
||||
<string name="follow_sets">Követési gyüjtemények</string>
|
||||
<string name="follow_sets">Követési listák</string>
|
||||
<string name="follow_sets_explainer">Ezek a követési listák az Ön saját alkalmazás-használatából lettek kialakítva. A felhasználókat privát vagy nyilvánosan követheti</string>
|
||||
<string name="labeled_bookmarks">Címkézett könyvjelzők</string>
|
||||
<string name="general_bookmarks">Általános könyvjelzők</string>
|
||||
<string name="follow_set_type_public">Nyílvános</string>
|
||||
@@ -458,14 +461,14 @@
|
||||
\nKoppintson a frissítéshez vagy érintse meg a hozzáadás gombot a gyüjtemény létrehozásához.
|
||||
</string>
|
||||
<string name="follow_set_create_btn_label">Új</string>
|
||||
<string name="follow_set_add_author_from_note_action">Szerző hozzáadása a követési gyűjteményhez</string>
|
||||
<string name="follow_set_add_author_from_note_action">Szerző hozzáadása a követési listához</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Felhasználó hozzáadása vagy eltávolítása a listákból, vagy új lista létrehozása ezzel a felhasználóval.</string>
|
||||
<string name="follow_set_icon_description">Ikon a(z) %1$s nevű listához</string>
|
||||
<string name="follow_set_icon_description">Ikon a követési listához</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s egy nyílvános tag</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s egy privát tag</string>
|
||||
<string name="follow_set_public_member_add_label">Hozzáadás mint nyílvános tag</string>
|
||||
<string name="follow_set_private_member_add_label">Hozzáadás mint privát tag</string>
|
||||
<string name="follow_set_public_members_header_label2">Publikus Tagok</string>
|
||||
<string name="follow_set_public_members_header_label2">Nyilvános tagok</string>
|
||||
<string name="follow_set_private_members_header_label2">Privát Tagok</string>
|
||||
<string name="follow_set_public_members_header_label">Nyílvános profilok</string>
|
||||
<string name="follow_set_private_members_header_label">Privát profilok</string>
|
||||
@@ -476,25 +479,27 @@
|
||||
<string name="follow_set_absence_indicator">A(z) %1$s nincs a listában</string>
|
||||
<string name="follow_set_absence_indicator2">%1$s nem tag</string>
|
||||
<string name="follow_set_man_dialog_title2">Saját listák és %1$s</string>
|
||||
<string name="follow_set_man_dialog_title">Saját követési gyüjtemények</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nem találhatók követési gyüjtemények, vagy nincs követési gyüjteménye. Érintse meg az alábbi gombot a frissítéshez vagy használja a menüt egy gyüjtemény létrehozásához.</string>
|
||||
<string name="follow_set_man_dialog_title">Saját listák</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nem találhatók vagy Önnek még nincs követési listája. Koppintson az alábbi gombra a frissítéshez vagy használja a menüt egy új lista létrehozásához.</string>
|
||||
<string name="follow_set_error_dialog_msg">Probléma történt a következő lekérdezésekor: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Új lista létrehozása</string>
|
||||
<string name="follow_set_creation_item_label">Új %1$s lista létrehozása felhasználóval</string>
|
||||
<string name="follow_set_creation_item_description">%1$s követési gyüjtemény létrehozása, és hozzáadás a következhöz: %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Új %1$s lista</string>
|
||||
<string name="follow_set_copy_dialog_title">Követési gyüjtemény másolása/klónozása</string>
|
||||
<string name="follow_set_creation_item_label">Új lista %1$s tagsággal</string>
|
||||
<string name="follow_set_creation_item_description">Egy új követési gyüjtemény létrehozása, és %1$s hozzáadása mint %2$s tag.</string>
|
||||
<string name="follow_set_creation_dialog_title">Új követési lista</string>
|
||||
<string name="follow_pack_creation_dialog_title">Új követési lista</string>
|
||||
<string name="follow_set_copy_dialog_title">Követési lista másolása/klónozása</string>
|
||||
<string name="follow_set_desc_modify_label">Leírás módosítása</string>
|
||||
<string name="follow_set_empty_desc_label">Ennek a listának nincs leírása</string>
|
||||
<string name="follow_set_current_desc_label">Jelenlegi leírás:</string>
|
||||
<string name="follow_set_copy_indicator_description">Az alábbiakban beállíthat egy egyéni nevet/leírást ehhez a klónozott követési gyűjteményhez.</string>
|
||||
<string name="follow_set_creation_name_label">Név megadása</string>
|
||||
<string name="follow_set_copy_name_label">(Gyüjtemény eredeti neve)</string>
|
||||
<string name="follow_set_creation_desc_label">Leírás megadása (nem kötelező)</string>
|
||||
<string name="follow_set_copy_desc_label">(Gyüjtemény eredeti leírása)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Gyüjtemény létrehozása</string>
|
||||
<string name="follow_set_copy_action_btn_label">Gyüjtemény másolása/klónozása</string>
|
||||
<string name="follow_set_rename_btn_label">Gyüjtemény átnevezése</string>
|
||||
<string name="follow_set_copy_indicator_description">Az alábbiakban beállíthat egy új nevet/leírást ehhez a klónozott követési listához.</string>
|
||||
<string name="follow_set_creation_name_label">Listanév</string>
|
||||
<string name="follow_set_copy_name_label">Új listanév</string>
|
||||
<string name="follow_set_creation_desc_label">Listaleírás (nem kötelező)</string>
|
||||
<string name="follow_set_copy_desc_label">Új listaleírás</string>
|
||||
<string name="follow_set_creation_action_btn_label">Lista létrehozása</string>
|
||||
<string name="follow_set_copy_action_btn_label">Lista másolása/klónozása</string>
|
||||
<string name="follow_set_rename_btn_label">Lista átnevezése</string>
|
||||
<string name="follow_set_edit_list_metadata">Lista szerkesztése</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Módosítás</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Ön átnevezi a követési gyüjteményt erről: </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> erre:</string>
|
||||
@@ -633,6 +638,7 @@
|
||||
<string name="followed_tags">Követett címke</string>
|
||||
<string name="relay_setup">Átjátszók</string>
|
||||
<string name="discover_follows">Követett csomagok</string>
|
||||
<string name="discover_follows_explainer">Ezek a listák azon felhasználók listái, akiket másoknak ajánlhat. Csak nyilvános felhasználók szerepelhetnek a listán.</string>
|
||||
<string name="discover_reads">Olvasmányok</string>
|
||||
<string name="discover_content_v2">Hírforrás-algoritmus</string>
|
||||
<string name="discover_marketplace">Piac</string>
|
||||
@@ -687,7 +693,7 @@
|
||||
<string name="zap_forward_title">Zap-ek továbbítása neki:</string>
|
||||
<string name="zap_forward_explainer">A funkciót támogató kliensek a Zap-eket az alábbi LN-címre vagy felhasználói profilra továbbítják az Öné helyett</string>
|
||||
<string name="geohash_title">A helyszín megjelenítése mint </string>
|
||||
<string name="geohash_explainer">Hozzáadja a helyének geohash-sét a bejegyzéséhez. A nyilvánosság tudni fogja, hogy a jelenlegi helytől 5 km-en (3 mi) belül tertózkodik</string>
|
||||
<string name="geohash_explainer">Hozzáadja a helyszínének geokivonatát a bejegyzéséhez. A nyilvánosság tudni fogja, hogy a jelenlegi helytől 5 km-en (3 mi) belül tertózkodik</string>
|
||||
<string name="geohash_exclusive">Helyszín-alapú bejegyzés</string>
|
||||
<string name="geohash_exclusive_explainer">Csak a helyszín követői láthatják. Az általános követők nem fogják látni.</string>
|
||||
<string name="hashtag_exclusive">Hashtag-exkluzív bejegyzés</string>
|
||||
@@ -714,15 +720,15 @@
|
||||
<string name="paste_from_clipboard">Beszúrás vágólapról</string>
|
||||
<string name="invalid_nip47_uri_title">Érvénytelen NIP-47 URI</string>
|
||||
<string name="invalid_nip47_uri_description">A(z) %1$s URI nem egy érvényes NIP-47 bejelentkezési URI.</string>
|
||||
<string name="connect_to_new_nwc_wallet">Csatlakozás az új NWC tárcához</string>
|
||||
<string name="couldnt_find_nwc_wallets">Tárca nem található</string>
|
||||
<string name="couldnt_find_nwc_wallets_description">Az Amethyst nem talált olyan pénztárcaalkalmazást, amely támogatja a Nostr Wallet Connect (NWC) szolgáltatást.
|
||||
<string name="connect_to_new_nwc_wallet">Kapcsolódás az új NWC-pénztárcához</string>
|
||||
<string name="couldnt_find_nwc_wallets">Nem található pénztárca</string>
|
||||
<string name="couldnt_find_nwc_wallets_description">Az Amethyst nem talált olyan pénztárca-alkalmazást, amely támogatja a Nostr Wallet Connect (NWC) szolgáltatást.
|
||||
\n\nAmit tehet:
|
||||
\n\t1. NWC támogatás ellenőrzése: Ellenőrizze, hogy a kívánt pénztárcaalkalmazás kompatibilis-e a Nostr Wallet Connect szolgáltatással.
|
||||
\n\t1. NWC-támogatás ellenőrzése: Ellenőrizze, hogy a kívánt pénztárca-alkalmazás kompatibilis-e a Nostr Wallet Connect szolgáltatással.
|
||||
\n\t2. Kapcsolat létrehozása: Nyissa meg a pénztárcaalkalmazást, és hozzon létre egy új NWC-kapcsolatot.
|
||||
\n\t3. Kapcsolódás az Amethysthez: A pénztárcája általában három lehetőséget kínál:
|
||||
\n\t\t3.1 Automatikus kapcsolódás: Egy gomb az Amethyst automatikus megnyitásához és csatlakoztatásához. Ha igen, kattintson erre a gombra, nyissa meg az Amethyst alkalmazást, és mentse el az új képernyőn.
|
||||
\n\t\t3.2 Manuális kapcsolódás: Egy kapcsolat URl, amelyet kimásolhat, és a képernyőn található beillesztés ikonra kattintva közvetlenül beírhatja.
|
||||
\n\t\t3.1 Automatikus kapcsolódás: Egy gomb az Amethyst automatikus megnyitásához és kapcsolódáshoz. Koppintson erre a gombra, nyissa meg az Amethyst alkalmazást, és mentse el az új képernyőn.
|
||||
\n\t\t3.2 Kézi kapcsolódás: Egy kapcsolódási cím, amelyet kimásolhat, és a képernyőn található beillesztés gombra koppintva közvetlenül beírhatja.
|
||||
\n\t\t3.3 QR-kód: A pénztárca létrehoz egy QR-kódot, amelyet a képernyőn található QR-kód gombról olvashat be.
|
||||
</string>
|
||||
<string name="language_description">Az alkalmazás felületéhez</string>
|
||||
@@ -1119,5 +1125,23 @@
|
||||
<string name="in_the_list">Már rajta van a listán</string>
|
||||
<string name="private_members">Privát tagok</string>
|
||||
<string name="public_members">Nyilvános tagok</string>
|
||||
<string name="private_members_count">Privát tagok (%1$s)</string>
|
||||
<string name="public_members_count">Nyilvános tagok (%1$s)</string>
|
||||
<string name="add_user_to_the_list">Felhasználó hozzáadása a listához</string>
|
||||
<string name="remove_user_from_the_list">Felhasználó törlése a listáról</string>
|
||||
<string name="follow_list_item_label">Követési csomag</string>
|
||||
<string name="members">Tagok</string>
|
||||
<string name="people_list_title">Követési lista metaadatai</string>
|
||||
<string name="people_list_explainer">A követési listák metaadatai a Nostr-on bárki számára láthatók. Csak a privát tagok adatai vannak titkosítva.</string>
|
||||
<string name="follow_pack_title">Követési csomag metaadatai</string>
|
||||
<string name="follow_pack_explainer">A követési csomag metaadatai a Nostr-on bárki számára láthatók, és gyakran megjelennek számos weboldalon, új felhasználók számára készült kezdőcsomagként.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Követési csomag szerkesztése</string>
|
||||
<string name="follow_pack_creation_name_label">Csomagnév</string>
|
||||
<string name="follow_pack_copy_name_label">Új csomagnév</string>
|
||||
<string name="follow_pack_creation_desc_label">Csomagleírás</string>
|
||||
<string name="follow_pack_copy_desc_label">Új csomagleírás</string>
|
||||
<string name="follow_set_broadcast">Közvetítési lista</string>
|
||||
<string name="follow_pack_broadcast">Közvetítési csomag</string>
|
||||
<string name="follow_set_delete">Lista törlése</string>
|
||||
<string name="follow_pack_delete">Csomag törlése</string>
|
||||
</resources>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Verzenden</string>
|
||||
<string name="save">Opslaan</string>
|
||||
<string name="create">Maken</string>
|
||||
<string name="rename">Hernoemen</string>
|
||||
<string name="cancel">Annuleren</string>
|
||||
<string name="failed_to_upload_the_image">Uploaden afbeelding mislukt</string>
|
||||
<string name="relay_address">Relay adres</string>
|
||||
@@ -124,6 +125,8 @@
|
||||
<string name="public_chat_feed">Publieke chats</string>
|
||||
<string name="global_feed">Globale feed</string>
|
||||
<string name="search_feed">Zoeken</string>
|
||||
<string name="search_and_add_a_user">Zoek en voeg gebruiker toe</string>
|
||||
<string name="add_a_user">Gebruiker toevoegen</string>
|
||||
<string name="add_a_relay">Voeg relay toe</string>
|
||||
<string name="profile_name">Naam</string>
|
||||
<string name="display_name">Naam</string>
|
||||
@@ -344,6 +347,7 @@
|
||||
<string name="report_dialog_post_report_btn">Rapporteer</string>
|
||||
<string name="report_dialog_title">Blokkeer en rapporteer</string>
|
||||
<string name="block_only">Blokkeren</string>
|
||||
<string name="manual_zaps">Handmatige Zap Splits</string>
|
||||
<string name="bookmarks">Bladwijzers</string>
|
||||
<string name="drafts">Concepten</string>
|
||||
<string name="private_bookmarks">Privé bladwijzers</string>
|
||||
@@ -441,10 +445,64 @@
|
||||
<string name="follow_list_selection">Volgerslijst</string>
|
||||
<string name="follow_list_kind3follows">Iedereen</string>
|
||||
<string name="follow_list_kind3follows_users_only">Volg alle gebruikers</string>
|
||||
<string name="follow_list_kind3_follows_users_only">Standaard volglijst</string>
|
||||
<string name="follow_list_kind3follows_proxy">Volgt via proxy</string>
|
||||
<string name="follow_list_aroundme">In de buurt</string>
|
||||
<string name="follow_list_global">Globaal</string>
|
||||
<string name="follow_list_mute_list">Mute-lijst</string>
|
||||
<string name="follow_sets">Volglijst</string>
|
||||
<string name="follow_sets_explainer">Dit zijn volglijsten ontworpen voor eigen gebruik. Je kunt gebruikers privé of publiekelijk volgen</string>
|
||||
<string name="labeled_bookmarks">Gelabelde bladwijzers</string>
|
||||
<string name="general_bookmarks">Algemene bladwijzers</string>
|
||||
<string name="follow_set_type_public">Openbaar</string>
|
||||
<string name="follow_set_type_private">Privé</string>
|
||||
<string name="follow_set_type_mixed">Gemengd</string>
|
||||
<string name="follow_set_empty_feed_msg"> Het lijkt erop dat je nog geen volgsets hebt.
|
||||
\nTik hieronder om te vernieuwen, of tik op de toevoegen knoppen om een nieuwe te maken.
|
||||
</string>
|
||||
<string name="follow_set_create_btn_label">Nieuw</string>
|
||||
<string name="follow_set_add_author_from_note_action">Voeg auteur toe aan volglijst</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Gebruiker toevoegen of verwijderen uit lijsten, of maak een nieuwe lijst aan met deze gebruiker.</string>
|
||||
<string name="follow_set_icon_description">Icoon voor volgset</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s is een openbaar lid</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s is een privélid</string>
|
||||
<string name="follow_set_public_member_add_label">Voeg toe als openbaar lid</string>
|
||||
<string name="follow_set_private_member_add_label">Als privélid toevoegen</string>
|
||||
<string name="follow_set_public_members_header_label2">Openbare leden</string>
|
||||
<string name="follow_set_private_members_header_label2">Privé leden</string>
|
||||
<string name="follow_set_public_members_header_label">Openbare profielen</string>
|
||||
<string name="follow_set_private_members_header_label">Privé profielen</string>
|
||||
<string name="follow_set_single_member_label">lid</string>
|
||||
<string name="follow_set_multiple_member_label">leden</string>
|
||||
<string name="follow_set_empty_label">Geen leden</string>
|
||||
<string name="follow_set_empty_label2">Leeg</string>
|
||||
<string name="follow_set_absence_indicator">%1$s staat niet in deze lijst</string>
|
||||
<string name="follow_set_absence_indicator2">%1$s is geen lid</string>
|
||||
<string name="follow_set_man_dialog_title2">Jouw lijsten en %1$s</string>
|
||||
<string name="follow_set_man_dialog_title">Jouw lijsten</string>
|
||||
<string name="follow_set_empty_dialog_msg">Er zijn geen volglijsten gevonden, of je hebt geen volglijsten. Tik hieronder om te vernieuwen of gebruik het menu om er een aan te maken.</string>
|
||||
<string name="follow_set_error_dialog_msg">Er was een probleem tijdens het ophalen: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Maak nieuwe lijst</string>
|
||||
<string name="follow_set_creation_item_label">Nieuwe lijst met %1$s lidmaatschap</string>
|
||||
<string name="follow_set_creation_item_description">Creëert een nieuwe volgset en voegt %1$s toe als lid van %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nieuwe volglijst</string>
|
||||
<string name="follow_pack_creation_dialog_title">Nieuwe volglijst</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopieer/kloon volglijst</string>
|
||||
<string name="follow_set_desc_modify_label">Beschrijving wijzigen</string>
|
||||
<string name="follow_set_empty_desc_label">Deze lijst heeft geen beschrijving</string>
|
||||
<string name="follow_set_current_desc_label">Huidige beschrijving:</string>
|
||||
<string name="follow_set_copy_indicator_description">Stel een nieuwe naam/omschrijving in voor de gekloonde lijst hieronder.</string>
|
||||
<string name="follow_set_creation_name_label">Lijstnaam</string>
|
||||
<string name="follow_set_copy_name_label">Nieuwe lijstnaam</string>
|
||||
<string name="follow_set_creation_desc_label">Lijstbeschrijving (optioneel)</string>
|
||||
<string name="follow_set_copy_desc_label">Beschrijving nieuwe lijst</string>
|
||||
<string name="follow_set_creation_action_btn_label">Lijst maken</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopieer/kloon lijst</string>
|
||||
<string name="follow_set_rename_btn_label">Lijst hernoemen</string>
|
||||
<string name="follow_set_edit_list_metadata">Bewerk lijst</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Aanpassen</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Je hernoemt de naam van </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> om..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Standaard poort is 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Verbind via Tor met Orbot
|
||||
\n\n1. Installeer [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -580,6 +638,7 @@
|
||||
<string name="followed_tags">Gevolgde tags</string>
|
||||
<string name="relay_setup">Relays</string>
|
||||
<string name="discover_follows">Volg pakketten</string>
|
||||
<string name="discover_follows_explainer">Dit zijn lijsten van gebruikers die u aanbeveelt aan andere mensen. Alleen openbare gebruikers zijn toegestaan.</string>
|
||||
<string name="discover_reads">Gelezen</string>
|
||||
<string name="discover_content_v2">Feed algoritmes</string>
|
||||
<string name="discover_marketplace">Marktplaats</string>
|
||||
@@ -661,6 +720,17 @@
|
||||
<string name="paste_from_clipboard">Plakken vanaf klembord</string>
|
||||
<string name="invalid_nip47_uri_title">Invalide NIP-47 URI</string>
|
||||
<string name="invalid_nip47_uri_description">De URI %1$s is geen geldige NIP-47 login-URI.</string>
|
||||
<string name="connect_to_new_nwc_wallet">Verbinding maken met nieuwe NWC-wallet</string>
|
||||
<string name="couldnt_find_nwc_wallets">Wallet niet gevonden</string>
|
||||
<string name="couldnt_find_nwc_wallets_description">Amethyst kon geen wallet-apps vinden die Nostr Wallet Connect (NWC) ondersteunen.
|
||||
\n\nWat je kan doen:
|
||||
\n\t1. Verifieer NWC Support: Controleer of uw voorkeurswallet app compatibel is met Nostr Wallet Connect.
|
||||
\n\t2. Maak een verbinding. Ga naar uw wallet-app en maak een nieuwe NWC verbinding aan.
|
||||
\n\t3. Link naar Amethyst: Je wallet geeft je meestal drie opties:
|
||||
\n\t\t3. Automatische link: Een knop om automatisch te openen en te verbinden met Amethyst. Als dat zo is, klik dan op die knop, open Amethist en druk op opslaan op het nieuwe scherm.
|
||||
\n\t\t3. Handmatige link: Een URI voor verbinding, u kunt het plakpictogram op dit scherm kopiëren en klikken om het direct in te voeren.
|
||||
\n\t\t3. QR-Code: De wallet maakt een QR-code die je kunt lezen op de QR-code knop op dit scherm.
|
||||
</string>
|
||||
<string name="language_description">Voor de interface van de app</string>
|
||||
<string name="theme_description">Donker-, Licht- of Systeemthema</string>
|
||||
<string name="automatically_load_images_gifs_description">Automatisch afbeeldingen en GIF\'s laden</string>
|
||||
@@ -832,6 +902,8 @@
|
||||
<string name="media_compression_quality_medium">Normaal</string>
|
||||
<string name="media_compression_quality_high">Hoog</string>
|
||||
<string name="media_compression_quality_uncompressed">Ongecomprimeerd</string>
|
||||
<string name="video_codec_h265_label">H.265/HEVC Codec gebruiken</string>
|
||||
<string name="video_codec_h265_description">Betere kwaliteit in kleinere bestandsformaten, maar niet alle apparaten ondersteunen H.265 playback.</string>
|
||||
<string name="edit_draft">Concept bewerken</string>
|
||||
<string name="login_with_qr_code">Inloggen met QR-Code</string>
|
||||
<string name="route">Route</string>
|
||||
@@ -890,6 +962,16 @@
|
||||
<string name="search_relays_not_found_description">Het maken van een relay lijst speciaal ontworpen voor zoekopdracht en gebruikers-tagging zal deze resultaten verbeteren.</string>
|
||||
<string name="search_relays_not_found_editing">Voeg tussen de 1 en 3 relays toe om te gebruiken bij het zoeken naar content of bij het taggen van gebruikers. Zorg ervoor dat uw gekozen relays implementatie van NIP-50</string>
|
||||
<string name="search_relays_not_found_examples">Goede opties zijn:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
|
||||
<string name="outbox_relays_title">Outbox relays</string>
|
||||
<string name="outbox_relays_not_found">Stel uw openbare outbox relays in om te posten</string>
|
||||
<string name="outbox_relays_not_found_description">Het maken van een relay-lijst speciaal ontworpen voor het ontvangen van jouw inhoud is cruciaal voor de Nostr ervaring en de enige manier waarop jouw volgers je kunnen vinden. </string>
|
||||
<string name="outbox_relays_not_found_editing">Voer tussen de 1 en 3 relays in om je berichten te ontvangen. Zorg ervoor dat je geen betaling nodig hebt als je niet betaalt om in te voegen</string>
|
||||
<string name="outbox_relays_not_found_examples">Goede opties zijn:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||
<string name="inbox_relays_title">Inbox relays</string>
|
||||
<string name="inbox_relays_not_found">Stel uw openbare inbox relays in om meldingen te ontvangen</string>
|
||||
<string name="inbox_relays_not_found_description">Het maken van een relay-lijst speciaal ontworpen voor het ontvangen van meldingen is cruciaal voor uw Nostr ervaring. </string>
|
||||
<string name="inbox_relays_not_found_editing">Voeg tussen de 1 en 3 relays toe die meldingen voor u ontvangen. Zorg ervoor dat ze geen betaling of vertrouwensweb van de afzender nodig hebben, anders ontvangt u alleen meldingen van betalers of langdurig gebruikers</string>
|
||||
<string name="inbox_relays_not_found_examples">Goede opties zijn:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
|
||||
<string name="dm_upload">DM uploaden</string>
|
||||
<string name="relay_settings">Relay-instellingen</string>
|
||||
<string name="public_home_section">Openbare thuis relays</string>
|
||||
@@ -1015,6 +1097,9 @@
|
||||
<string name="torrent_failure">Kon bestand niet openen</string>
|
||||
<string name="torrent_no_apps">Geen torrent apps geïnstalleerd om het bestand te openen en te downloaden.</string>
|
||||
<string name="torrent_no_info">Evenement heeft niet genoeg informatie om een magnet-link te maken</string>
|
||||
<string name="my_lists_and_sets">Mijn lijsten/sets</string>
|
||||
<string name="my_lists">Mijn lijsten</string>
|
||||
<string name="people_list_label">Gebruikers</string>
|
||||
<string name="select_list_to_filter">Selecteer een lijst om de feed te filteren</string>
|
||||
<string name="temporary_account">Afmelden bij apparaatvergrendeling</string>
|
||||
<string name="private_message">Privébericht</string>
|
||||
@@ -1036,4 +1121,27 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Wilt u het recente crashrapport verzenden naar Amethyst in een DM? Er zal geen persoonlijke informatie gedeeld worden</string>
|
||||
<string name="crashreport_found_send">Verzenden</string>
|
||||
<string name="this_message_will_disappear_in_days">Dit bericht zal verdwijnen in %1$d dagen</string>
|
||||
<string name="select_signer">Selecteer ondertekenaar</string>
|
||||
<string name="in_the_list">Al in de lijst</string>
|
||||
<string name="private_members">Privé leden</string>
|
||||
<string name="public_members">Openbare leden</string>
|
||||
<string name="private_members_count">Privéleden (%1$s)</string>
|
||||
<string name="public_members_count">Openbare leden (%1$s)</string>
|
||||
<string name="add_user_to_the_list">Gebruiker toevoegen aan de lijst</string>
|
||||
<string name="remove_user_from_the_list">Gebruiker uit de lijst verwijderen</string>
|
||||
<string name="follow_list_item_label">Volgpakket</string>
|
||||
<string name="members">Leden</string>
|
||||
<string name="people_list_title">Volglijst metadata</string>
|
||||
<string name="people_list_explainer">Volglijsten metadata kunnen door iedereen op Nostr. Alleen uw privéleden zijn versleuteld.</string>
|
||||
<string name="follow_pack_title">Volglijst metadata</string>
|
||||
<string name="follow_pack_explainer">Volgpakket metadata kan door iedereen op Nostr worden gezien en is vaak op veel websites gepubliceerd als opstartuitrustingen voor nieuwe gebruikers.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Wijzig volglijst</string>
|
||||
<string name="follow_pack_creation_name_label">Pakketnaam</string>
|
||||
<string name="follow_pack_copy_name_label">Nieuwe pakketnaam</string>
|
||||
<string name="follow_pack_creation_desc_label">Pakketbeschrijving</string>
|
||||
<string name="follow_pack_copy_desc_label">Nieuwe pakketbeschrijving</string>
|
||||
<string name="follow_set_broadcast">Uitzendlijst</string>
|
||||
<string name="follow_pack_broadcast">Uitzendpakket</string>
|
||||
<string name="follow_set_delete">Verwijder lijst</string>
|
||||
<string name="follow_pack_delete">Verwijder pakket</string>
|
||||
</resources>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_upload">Używasz klucza publicznego i kluczy publicznych tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc wgrać</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_sign_events">Używasz klucza publicznego i kluczy publicznych tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc zapisać się na udział w wydarzeniu</string>
|
||||
<string name="unauthorized_exception">Nieautoryzowane odszyfrowywanie</string>
|
||||
<string name="unauthorized_exception_description">Sygnatariusz nie autoryzował deszyfrowania wymaganego do wykonania tej operacji. Aktywuj deszyfrowanie NIP-44 w aplikacji podpisującej i spróbuj ponownie</string>
|
||||
<string name="unauthorized_exception_description">Sygnatariusz nie autoryzował deszyfrowania wymaganego do wykonania tej operacji. Aktywuj deszyfrowanie NIP-44 w aplikacji logującej i spróbuj ponownie</string>
|
||||
<string name="signer_not_found_exception">Nie odnaleziono sygnatariusza</string>
|
||||
<string name="signer_not_found_exception_description">Czy aplikacja sygnatariusza została odinstalowana? Sprawdź, czy aplikacja sygnatariusza jest zainstalowana i czy ma to konto. Wyloguj się i zaloguj ponownie, jeśli aplikacja sygnatariusza uległa zmianie.</string>
|
||||
<string name="zaps">Zapy</string>
|
||||
@@ -68,9 +68,9 @@
|
||||
<string name="quote">Zacytuj</string>
|
||||
<string name="fork">Sklonuj</string>
|
||||
<string name="propose_an_edit">Zaproponuj zmianę</string>
|
||||
<string name="new_amount_in_sats">Nowa kwota w Satach</string>
|
||||
<string name="new_amount_in_sats">Nowa kwota w Satsach</string>
|
||||
<string name="add">Dodaj</string>
|
||||
<string name="replying_to">"odpowiadając do"</string>
|
||||
<string name="replying_to">"odpowiadając do "</string>
|
||||
<string name="and">" i "</string>
|
||||
<string name="in_channel">"na kanale "</string>
|
||||
<string name="profile_banner">Baner profilu</string>
|
||||
@@ -80,7 +80,7 @@
|
||||
<string name="followers">" Obserwujące"</string>
|
||||
<string name="number_following">"%1$s Obserwowanych"</string>
|
||||
<string name="number_followers">"%1$s Obserwujących"</string>
|
||||
<string name="profile">Twój Profil</string>
|
||||
<string name="profile">Mój Profil</string>
|
||||
<string name="security_filters">Filtry bezpieczeństwa</string>
|
||||
<string name="log_out">Wyloguj się</string>
|
||||
<string name="show_more">Pokaż Więcej</string>
|
||||
@@ -344,6 +344,7 @@
|
||||
<string name="report_dialog_post_report_btn">Wyślij zgłoszenie</string>
|
||||
<string name="report_dialog_title">Zablokuj i zgłoś</string>
|
||||
<string name="block_only">Zablokuj</string>
|
||||
<string name="manual_zaps">Ręczny podział zapów</string>
|
||||
<string name="bookmarks">Zakładki</string>
|
||||
<string name="drafts">Projekty</string>
|
||||
<string name="private_bookmarks">Prywatne Zakładki</string>
|
||||
@@ -440,58 +441,62 @@
|
||||
<string name="no">Nie</string>
|
||||
<string name="follow_list_selection">Lista obserwowanych</string>
|
||||
<string name="follow_list_kind3follows">Obserwowane</string>
|
||||
<string name="follow_list_kind3follows_users_only">Wszyscy obserwujący</string>
|
||||
<string name="follow_list_kind3follows_users_only">Obserwujące</string>
|
||||
<string name="follow_list_kind3_follows_users_only">Domyślna lista obserwowanych</string>
|
||||
<string name="follow_list_kind3follows_proxy">Obserwuje przez proxy</string>
|
||||
<string name="follow_list_aroundme">W pobliżu</string>
|
||||
<string name="follow_list_global">Wszystkie</string>
|
||||
<string name="follow_list_mute_list">Zablokowane</string>
|
||||
<string name="follow_sets">Zbiory obserwowanych</string>
|
||||
<string name="follow_sets">Listy obserwowanych</string>
|
||||
<string name="follow_sets_explainer">Są to listy obserwowanych przeznaczone do prywatnego użytku. Możesz obserwować użytkowników dyskretnie lub otwarcie</string>
|
||||
<string name="labeled_bookmarks">Oznaczone zakładki</string>
|
||||
<string name="general_bookmarks">Ogólne zakładki</string>
|
||||
<string name="follow_set_type_public">Publiczna</string>
|
||||
<string name="follow_set_type_private">Prywatna</string>
|
||||
<string name="follow_set_type_mixed">Mieszana</string>
|
||||
<string name="follow_set_empty_feed_msg"> Wygląda na to, że nie masz jeszcze żadnych zbiorów obserwowanych.
|
||||
\nDotknij poniżej, aby odświeżyć, lub naciśnij przycisk Dodaj, aby utworzyć nowy.
|
||||
<string name="follow_set_empty_feed_msg">Wygląda na to, że nie masz jeszcze żadnej listy obserwowanych.
|
||||
\nDotknij poniżej, aby odświeżyć, lub naciśnij przycisk Dodaj, aby utworzyć listę.
|
||||
</string>
|
||||
<string name="follow_set_create_btn_label">Nowy</string>
|
||||
<string name="follow_set_add_author_from_note_action">Dodaj autora do zbioru obserwowanych</string>
|
||||
<string name="follow_set_create_btn_label">Nowa</string>
|
||||
<string name="follow_set_add_author_from_note_action">Dodaj autora do listy obserwowanych</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Dodaj lub usuń użytkownika z list, lub utwórz nową listę z tym użytkownikiem.</string>
|
||||
<string name="follow_set_icon_description">Ikona dla listy %1$s</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s jest uczestnikiem publicznym</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s jest uczestnikiem prywatnym</string>
|
||||
<string name="follow_set_public_member_add_label">Dodaj jako uczestnika publicznego</string>
|
||||
<string name="follow_set_private_member_add_label">Dodaj jako uczestnika prywatnego</string>
|
||||
<string name="follow_set_public_members_header_label2">Uczestnicy publiczni</string>
|
||||
<string name="follow_set_private_members_header_label2">Prywatni uczestnicy</string>
|
||||
<string name="follow_set_public_members_header_label2">Publiczne</string>
|
||||
<string name="follow_set_private_members_header_label2">Prywatne</string>
|
||||
<string name="follow_set_public_members_header_label">Profile publiczne</string>
|
||||
<string name="follow_set_private_members_header_label">Profile prywatne</string>
|
||||
<string name="follow_set_single_member_label">uczestnik</string>
|
||||
<string name="follow_set_multiple_member_label">uczestnicy</string>
|
||||
<string name="follow_set_multiple_member_label">członkowie</string>
|
||||
<string name="follow_set_empty_label">Brak uczestników</string>
|
||||
<string name="follow_set_empty_label2">Pusty</string>
|
||||
<string name="follow_set_empty_label2">Pusto</string>
|
||||
<string name="follow_set_absence_indicator">%1$s nie jest na liście</string>
|
||||
<string name="follow_set_absence_indicator2">%1$s nie jest uczestnikiem</string>
|
||||
<string name="follow_set_man_dialog_title2">Twoje listy oraz %1$s</string>
|
||||
<string name="follow_set_man_dialog_title">Twój zbiór obserwowanych</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nie znaleziono zbiorów obserwowanych lub nie masz żadnych zbiorów obserwowanych. Dotknij poniżej, aby odświeżyć lub użyj menu, aby go utworzyć.</string>
|
||||
<string name="follow_set_man_dialog_title2">%1$s na Twoich listach</string>
|
||||
<string name="follow_set_man_dialog_title">Twoja lista obserwowanych</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nie znaleziono list obserwowanych lub nie masz żadnych list obserwowanych. Naciśnij poniżej, aby odświeżyć lub użyj menu, aby utworzyć nową.</string>
|
||||
<string name="follow_set_error_dialog_msg">Podczas pobierania wystąpił błąd: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Utwórz nową listę</string>
|
||||
<string name="follow_set_creation_item_label">\"Utwórz nową listę %1$s z użytkownikiem</string>
|
||||
<string name="follow_set_creation_item_description">Tworzy zbiór obserwowanych %1$s i dodaje do niego %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nowa lista %1$s</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopiuj/Klonuj zbiór obserwowanych</string>
|
||||
<string name="follow_set_creation_item_label">\"Utwórz nową listę %1$s z członkiem</string>
|
||||
<string name="follow_set_creation_item_description">Tworzy zestaw obserwowanych %1$s i dodaje do niego %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nowa lista</string>
|
||||
<string name="follow_pack_creation_dialog_title">Nowa Kategoria</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopiuj/Sklonuj listę obserwowanych</string>
|
||||
<string name="follow_set_desc_modify_label">Modyfikuj opis</string>
|
||||
<string name="follow_set_empty_desc_label">Ta lista nie ma opisu</string>
|
||||
<string name="follow_set_current_desc_label">Aktualny opis:</string>
|
||||
<string name="follow_set_copy_indicator_description">Możesz wybrać własną nazwę/opis dla sklonowanego zbioru poniżej.</string>
|
||||
<string name="follow_set_creation_name_label">Nazwa zbioru</string>
|
||||
<string name="follow_set_copy_indicator_description">Ustaw nową nazwę/opis dla sklonowanej listy poniżej.</string>
|
||||
<string name="follow_set_creation_name_label">Nazwa listy</string>
|
||||
<string name="follow_set_copy_name_label">(Nazwa Oryginalna)</string>
|
||||
<string name="follow_set_creation_desc_label">Opis zbioru (opcjonalnie)</string>
|
||||
<string name="follow_set_copy_desc_label">(Opis oryginalnego zbioru)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Utwórz zbiór</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopiuj/Klonuj zbiór</string>
|
||||
<string name="follow_set_rename_btn_label">Zmień nazwę zbioru</string>
|
||||
<string name="follow_set_creation_desc_label">Opis listy (opcjonalnie)</string>
|
||||
<string name="follow_set_copy_desc_label">Opis nowej listy</string>
|
||||
<string name="follow_set_creation_action_btn_label">Utwórz listę</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopiuj/Sklonuj listę</string>
|
||||
<string name="follow_set_rename_btn_label">Zmień nazwę listy</string>
|
||||
<string name="follow_set_edit_list_metadata">Edytuj listę</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Modyfikuj</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Zmieniasz nazwę z </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> do..</string>
|
||||
@@ -630,6 +635,7 @@
|
||||
<string name="followed_tags">Obserwowane tagi</string>
|
||||
<string name="relay_setup">Transmitery</string>
|
||||
<string name="discover_follows">Godne uwagi</string>
|
||||
<string name="discover_follows_explainer">Są to listy profili, które polecasz innym użytkownikom. Dozwolone są wyłącznie profile publiczne.</string>
|
||||
<string name="discover_reads">Popularne</string>
|
||||
<string name="discover_content_v2">Wybrane przez algorytm</string>
|
||||
<string name="discover_marketplace">Market</string>
|
||||
@@ -777,11 +783,11 @@
|
||||
<string name="lightning_wallets_not_found2">Nie znaleziono portfeli Lightning</string>
|
||||
<string name="paid">Zapłacone</string>
|
||||
<string name="wallet_number">Portfel %1$s</string>
|
||||
<string name="error_opening_external_signer">Błąd podczas otwierania aplikacji podpisującej</string>
|
||||
<string name="error_opening_external_signer_description">Nie można odnaleźć aplikacji podpisującej. Sprawdź, czy aplikacja nie została odinstalowana</string>
|
||||
<string name="error_opening_external_signer">Błąd podczas otwierania aplikacji logującej</string>
|
||||
<string name="error_opening_external_signer_description">Nie można odnaleźć aplikacji logującej. Sprawdź, czy aplikacja nie została odinstalowana</string>
|
||||
<string name="sign_request_rejected2">Prośba o zalogowanie została odrzucona</string>
|
||||
<string name="sign_request_rejected">Odrzucono aplikację podpisującego</string>
|
||||
<string name="sign_request_rejected_description">Upewnij się, że aplikacja podpisującego autoryzuje tę transakcję</string>
|
||||
<string name="sign_request_rejected">Prośba o zalogowanie odrzucona</string>
|
||||
<string name="sign_request_rejected_description">Upewnij się, że aplikacja logującego autoryzuje tę operację</string>
|
||||
<string name="no_wallet_found_with_error">Nie znaleziono portfeli do zapłacenia faktury z Lightning (Error: %1$s). Proszę zainstalować Lightning wallet, aby używać zapów</string>
|
||||
<string name="no_wallet_found">Nie znaleziono portfeli do zapłacenia faktury z Lightning. Proszę zainstalować Lightning wallet, aby używać zapów</string>
|
||||
<string name="hidden_words">Ukryte słowa</string>
|
||||
@@ -1088,10 +1094,10 @@
|
||||
<string name="torrent_failure">Nie udało się otworzyć pliku</string>
|
||||
<string name="torrent_no_apps">Brak zainstalowanych aplikacji torrent do otwarcia i pobrania pliku.</string>
|
||||
<string name="torrent_no_info">Zdarzenie nie ma wystarczającej ilości informacji, aby zbudować link magnetyczny</string>
|
||||
<string name="my_lists_and_sets">Moje Listy/Zbiory</string>
|
||||
<string name="my_lists_and_sets">Moje Listy/Zestawy</string>
|
||||
<string name="my_lists">Moje listy</string>
|
||||
<string name="people_list_label">Użytkownicy</string>
|
||||
<string name="select_list_to_filter">Wybierz listę, aby filtrować aktualności</string>
|
||||
<string name="select_list_to_filter">Wybierz listę profili, aby filtrować aktualności</string>
|
||||
<string name="temporary_account">Wyloguj się przy blokowaniu urządzenia</string>
|
||||
<string name="private_message">Wiadomość prywatna</string>
|
||||
<string name="public_message">Publiczna wiadomość</string>
|
||||
@@ -1116,6 +1122,23 @@
|
||||
<string name="in_the_list">Już jest na liście</string>
|
||||
<string name="private_members">Prywatni uczestnicy</string>
|
||||
<string name="public_members">Uczestnicy publiczni</string>
|
||||
<string name="private_members_count">Uczestnicy prywatni (%1$s)</string>
|
||||
<string name="public_members_count">Uczestnicy publiczni (%1$s)</string>
|
||||
<string name="add_user_to_the_list">Dodaj użytkownika do listy</string>
|
||||
<string name="remove_user_from_the_list">Usuń użytkownika z listy</string>
|
||||
<string name="follow_list_item_label">Kategoria profilów</string>
|
||||
<string name="members">Uczestnicy</string>
|
||||
<string name="people_list_title">Metadane listy profili</string>
|
||||
<string name="people_list_explainer">Metadane listy obserwowanych są widoczne dla wszystkich użytkowników w sieci Nostr. Tylko Twoi uczestnicy prywatni są zaszyfrowani.</string>
|
||||
<string name="follow_pack_title">Metadane kategorii profilów</string>
|
||||
<string name="follow_pack_explainer">Metadane kategorii profilów są widoczne dla wszystkich użytkowników serwisu Nostr i często publikowane na wielu stronach internetowych jako zestawy startowe dla nowych użytkowników.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Edytuj kategorię profili</string>
|
||||
<string name="follow_pack_creation_name_label">Nazwa kategorii</string>
|
||||
<string name="follow_pack_copy_name_label">Nowa nazwa kategorii</string>
|
||||
<string name="follow_pack_creation_desc_label">Opis kategorii</string>
|
||||
<string name="follow_pack_copy_desc_label">Opis nowej kategorii</string>
|
||||
<string name="follow_set_broadcast">Upowszechnij listę</string>
|
||||
<string name="follow_pack_broadcast">Upowszechnij kategorię</string>
|
||||
<string name="follow_set_delete">Usuń listę</string>
|
||||
<string name="follow_pack_delete">Usuń kategorię</string>
|
||||
</resources>
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
<string name="report_dialog_post_report_btn">Enviar Denuncia</string>
|
||||
<string name="report_dialog_title">Bloquear e Denunciar</string>
|
||||
<string name="block_only">Bloquear</string>
|
||||
<string name="manual_zaps">Divisão manual de Zap</string>
|
||||
<string name="bookmarks">Itens Salvos</string>
|
||||
<string name="drafts">Rascunhos</string>
|
||||
<string name="private_bookmarks">Itens Salvos Privados</string>
|
||||
@@ -448,6 +449,7 @@
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista Silenciada</string>
|
||||
<string name="follow_sets">Listas de seguidores</string>
|
||||
<string name="follow_sets_explainer">Essas são listas de seguidores criadas para seu próprio uso. Você pode seguir usuários de forma privada ou pública.</string>
|
||||
<string name="labeled_bookmarks">Favoritos com etiqueta</string>
|
||||
<string name="general_bookmarks">Favoritos gerais</string>
|
||||
<string name="follow_set_type_public">Público</string>
|
||||
@@ -480,6 +482,7 @@
|
||||
<string name="follow_set_creation_item_label">Criar nova lista %1$s com usuário</string>
|
||||
<string name="follow_set_creation_item_description">Cria um conjunto de seguimento %1$s e adiciona %2$s a ele.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nova lista de seguidores</string>
|
||||
<string name="follow_pack_creation_dialog_title">Novo pacote de recomendações</string>
|
||||
<string name="follow_set_copy_dialog_title">Copiar/Clonar lista de seguidores</string>
|
||||
<string name="follow_set_desc_modify_label">Modificar descrição</string>
|
||||
<string name="follow_set_empty_desc_label">Esta lista não tem descrição</string>
|
||||
@@ -492,6 +495,7 @@
|
||||
<string name="follow_set_creation_action_btn_label">Criar lista</string>
|
||||
<string name="follow_set_copy_action_btn_label">Copiar/Clonar lista</string>
|
||||
<string name="follow_set_rename_btn_label">Renomear lista</string>
|
||||
<string name="follow_set_edit_list_metadata">Editar lista</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Modificar</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Você está renomeando de </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> para..</string>
|
||||
@@ -630,6 +634,7 @@
|
||||
<string name="followed_tags">Tags Seguidas</string>
|
||||
<string name="relay_setup">Relés</string>
|
||||
<string name="discover_follows">Pacotes Seguir</string>
|
||||
<string name="discover_follows_explainer">Essas são listas de usuários que você recomenda para outras pessoas. Somente usuários públicos são permitidos.</string>
|
||||
<string name="discover_reads">Leituras</string>
|
||||
<string name="discover_content_v2">Algoritmos de feed</string>
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
@@ -1121,4 +1126,17 @@
|
||||
<string name="remove_user_from_the_list">Remover usuário da lista</string>
|
||||
<string name="follow_list_item_label">Pacote Seguir</string>
|
||||
<string name="members">Membros</string>
|
||||
<string name="people_list_title">Metadados da lista de seguidores</string>
|
||||
<string name="people_list_explainer">Os metadados das listas de seguidores podem ser vistos por qualquer pessoa no Nostr. Apenas os membros privados são criptografados.</string>
|
||||
<string name="follow_pack_title">Metadados do pacote de recomendações</string>
|
||||
<string name="follow_pack_explainer">Os metadados dos pacotes de recomendações podem ser vistos por qualquer pessoa no Nostr e são frequentemente publicados em sites como kits iniciais para novos usuários.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Editar pacote de recomendações</string>
|
||||
<string name="follow_pack_creation_name_label">Nome do pacote</string>
|
||||
<string name="follow_pack_copy_name_label">Novo nome do pacote</string>
|
||||
<string name="follow_pack_creation_desc_label">Descrição do pacote</string>
|
||||
<string name="follow_pack_copy_desc_label">Nova descrição do pacote</string>
|
||||
<string name="follow_set_broadcast">Transmitir lista</string>
|
||||
<string name="follow_pack_broadcast">Transmitir pacote de recomendações</string>
|
||||
<string name="follow_set_delete">Excluir lista</string>
|
||||
<string name="follow_pack_delete">Excluir pacote de recomendações</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
<string name="illegal_behavior">Nedovoljeno obnašanje</string>
|
||||
<string name="other">Drugo</string>
|
||||
<string name="harassment">Nadlegovanje</string>
|
||||
<string name="unknown">Neznano</string>
|
||||
<string name="unknown">Nepoznano</string>
|
||||
<string name="relay_icon">Ikona releja</string>
|
||||
<string name="unknown_author">Avtor neznan</string>
|
||||
<string name="copy_text">Kopiraj tekst</string>
|
||||
<string name="copy_user_pubkey">Kopiraj ID avtorja</string>
|
||||
<string name="copy_note_id">Kopiraj ID zapiska</string>
|
||||
<string name="broadcast">Oddajanje</string>
|
||||
<string name="broadcast">Razpošlji</string>
|
||||
<string name="timestamp_it">Uporabi časovni žig</string>
|
||||
<string name="timestamp_pending">Časovni žig: Potrditve v obdelavi</string>
|
||||
<string name="timestamp_pending_short">OTS: V obdelavi</string>
|
||||
@@ -89,7 +89,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="error_parsing_error_message">Napaka pri razčlembi sporočila o napaki</string>
|
||||
<string name="following">" Sledi"</string>
|
||||
<string name="followers">" Sledilcev"</string>
|
||||
<string name="number_following">"%1$s Sledi"</string>
|
||||
<string name="number_following">"%1$s Sledim"</string>
|
||||
<string name="number_followers">"%1$s Sledilcev"</string>
|
||||
<string name="profile">Profil</string>
|
||||
<string name="security_filters">Varnostni filtri</string>
|
||||
@@ -102,11 +102,11 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="thank_you_so_much">Najlepša hvala!</string>
|
||||
<string name="amount_in_sats">Vsota v Sat</string>
|
||||
<string name="send_sats">Pošlji Sat</string>
|
||||
<string name="secret_emoji_maker">Orodje za emodžije s skrivnostmi</string>
|
||||
<string name="secret_emoji_maker_explainer">Izberi emodži za pošiljanje s skritim sporočilom</string>
|
||||
<string name="secret_note_to_receiver">Sprejem skrivnosti</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Moja skrita sporočila</string>
|
||||
<string name="secret_visible_text">Vidni prefiksi</string>
|
||||
<string name="secret_emoji_maker">Orodje za emoji s skrivnostmi</string>
|
||||
<string name="secret_emoji_maker_explainer">Izberi emoji za pošiljanje s skritim sporočilom</string>
|
||||
<string name="secret_note_to_receiver">Skrivno sporočilo</string>
|
||||
<string name="secret_note_to_receiver_placeholder">Moje skrito sporočilo</string>
|
||||
<string name="secret_visible_text">Vidna predpona</string>
|
||||
<string name="secret_visible_text_placeholder">😎</string>
|
||||
<string name="secret_add_to_text">Dodaj v zapisek</string>
|
||||
<string name="error_parsing_preview_for">"Napaka pri razčlembi predogleda za %1$s : %2$s"</string>
|
||||
@@ -173,11 +173,11 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Kopiraj ID kanala (zapisek) v odložišče</string>
|
||||
<string name="edits_the_channel_metadata">Uredi metapodatke kanala</string>
|
||||
<string name="join">Pridruži se</string>
|
||||
<string name="known">Znano</string>
|
||||
<string name="known">Poznani</string>
|
||||
<string name="new_requests">Nova prošnja</string>
|
||||
<string name="blocked_users">Blokirani uporabniki</string>
|
||||
<string name="new_threads">Nove objave</string>
|
||||
<string name="conversations">Pogovori</string>
|
||||
<string name="conversations">Razprave</string>
|
||||
<string name="feed">Vsebine</string>
|
||||
<string name="mod_queue">Moderatorska vrsta</string>
|
||||
<string name="notes">Zapiski</string>
|
||||
@@ -185,8 +185,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="mutual">Vaše</string>
|
||||
<string name="gallery">Galerija</string>
|
||||
<string name="follows">"sledilcev"</string>
|
||||
<string name="reports">"Reportaže"</string>
|
||||
<string name="number_reports">"%1$s Prijave"</string>
|
||||
<string name="reports">"Prijave"</string>
|
||||
<string name="number_reports">"%1$s Prijav"</string>
|
||||
<string name="more_options">Več možnosti</string>
|
||||
<string name="relays">" Releji"</string>
|
||||
<string name="website">Spletna stran</string>
|
||||
@@ -199,7 +199,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Kopiraj javni ključ (NPub) v odložišče</string>
|
||||
<string name="send_a_direct_message">Pošlji direktno sporočilo</string>
|
||||
<string name="edits_the_user_s_metadata">Uredi uporabnikove metapodatke</string>
|
||||
<string name="follow">Sledim</string>
|
||||
<string name="follow">Že sledim</string>
|
||||
<string name="follow_back">Sledi nazaj</string>
|
||||
<string name="unblock">Deblokiraj</string>
|
||||
<string name="copy_user_id">Kopiraj uporabnikov ID</string>
|
||||
@@ -358,6 +358,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="report_dialog_post_report_btn">Objavi prijavo</string>
|
||||
<string name="report_dialog_title">Blokiraj in prijavi</string>
|
||||
<string name="block_only">Blokiraj</string>
|
||||
<string name="manual_zaps">Ročno razdeli zape</string>
|
||||
<string name="bookmarks">Zaznamki</string>
|
||||
<string name="drafts">Osnutki</string>
|
||||
<string name="private_bookmarks">Privatni zaznamki</string>
|
||||
@@ -452,7 +453,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Vaši podatki bodo takoj preneseni v običajno omrežje</string>
|
||||
<string name="yes">Da</string>
|
||||
<string name="no">Ne</string>
|
||||
<string name="follow_list_selection">Spisek komu sledimo</string>
|
||||
<string name="follow_list_selection">Seznam sledenih</string>
|
||||
<string name="follow_list_kind3follows">Vse, čemur sledimo</string>
|
||||
<string name="follow_list_kind3follows_users_only">Vsi sledeni sledenih</string>
|
||||
<string name="follow_list_kind3_follows_users_only">Privzeti seznam sledenih</string>
|
||||
@@ -461,6 +462,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="follow_list_global">Globalno</string>
|
||||
<string name="follow_list_mute_list">Spisek utišanih</string>
|
||||
<string name="follow_sets">Seznam sledenih</string>
|
||||
<string name="follow_sets_explainer">To so seznami sledenih, namenjeni za vašo lastno uporabo. Uporabnike lahko sledite javno ali zasebno</string>
|
||||
<string name="labeled_bookmarks">Označeni zaznamki</string>
|
||||
<string name="general_bookmarks">Splošni zaznamki</string>
|
||||
<string name="follow_set_type_public">Javno</string>
|
||||
@@ -495,6 +497,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="follow_set_creation_item_label">Nov seznam s %1$s člani</string>
|
||||
<string name="follow_set_creation_item_description">Ustvari nov set sledenih, ter doda %1$s kot %2$s člana.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nov seznam sledenih</string>
|
||||
<string name="follow_pack_creation_dialog_title">Nov paket sledenih</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopiraj/Kloniraj seznam sledenih</string>
|
||||
<string name="follow_set_desc_modify_label">Spremeni opis</string>
|
||||
<string name="follow_set_empty_desc_label">Ta seznam nima opisa</string>
|
||||
@@ -507,6 +510,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="follow_set_creation_action_btn_label">Ustvari seznam</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopiraj/kloniraj seznam</string>
|
||||
<string name="follow_set_rename_btn_label">Preimenuj seznam</string>
|
||||
<string name="follow_set_edit_list_metadata">Uredi seznam</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Uredi</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Preimenuješ iz </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> v..</string>
|
||||
@@ -645,6 +649,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="followed_tags">Spremljana vsebina</string>
|
||||
<string name="relay_setup">Releji</string>
|
||||
<string name="discover_follows">Paketi sledenih</string>
|
||||
<string name="discover_follows_explainer">To so seznami uporabnikov, ki jih priporočate drugim. Dovoljeni so le javni uporabniki.</string>
|
||||
<string name="discover_reads">Branje</string>
|
||||
<string name="discover_content_v2">Algoritmi vsebin</string>
|
||||
<string name="discover_marketplace">Tržnica</string>
|
||||
@@ -1138,4 +1143,17 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="remove_user_from_the_list">Odstrani uporabnika iz seznama</string>
|
||||
<string name="follow_list_item_label">Paket sledenih</string>
|
||||
<string name="members">Člani</string>
|
||||
<string name="people_list_title">Sledi metapodatkom seznama</string>
|
||||
<string name="people_list_explainer">Sledenje metapodatkom seznama je lahko vidno vsem na Nostru. Le tvoji zasebni člani so šifrirani.</string>
|
||||
<string name="follow_pack_title">Metapodatki paketa sledenih</string>
|
||||
<string name="follow_pack_explainer">Metapodatki paketa sledenih so lahko vidno vsem na Nostru in redno objavljeni na ostalih spletnih straneh, kot začetni prikaz za nove uporabnike</string>
|
||||
<string name="follow_pack_edit_list_metadata">Uredi paket sledenih</string>
|
||||
<string name="follow_pack_creation_name_label">Ime paketa</string>
|
||||
<string name="follow_pack_copy_name_label">Novo ime paketa</string>
|
||||
<string name="follow_pack_creation_desc_label">Opis paketa</string>
|
||||
<string name="follow_pack_copy_desc_label">Nov opis paketa</string>
|
||||
<string name="follow_set_broadcast">Oddajaj seznam</string>
|
||||
<string name="follow_pack_broadcast">Oddajaj paket</string>
|
||||
<string name="follow_set_delete">Izbriši seznam</string>
|
||||
<string name="follow_pack_delete">Izbriši paket</string>
|
||||
</resources>
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
<string name="report_dialog_post_report_btn">Skicka rapport</string>
|
||||
<string name="report_dialog_title">Blockera och rapportera</string>
|
||||
<string name="block_only">Blockera</string>
|
||||
<string name="manual_zaps">Manuell Zap delning</string>
|
||||
<string name="bookmarks">Bokmärken</string>
|
||||
<string name="drafts">Utkast</string>
|
||||
<string name="private_bookmarks">Privata Bokmärken</string>
|
||||
@@ -448,6 +449,7 @@
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Tyst listan</string>
|
||||
<string name="follow_sets">Följelistor</string>
|
||||
<string name="follow_sets_explainer">Detta är följlistor avsedda för ditt eget bruk. Du kan följa användare privat eller offentligt.</string>
|
||||
<string name="labeled_bookmarks">Märkta bokmärken</string>
|
||||
<string name="general_bookmarks">Allmänna bokmärken</string>
|
||||
<string name="follow_set_type_public">Offentlig</string>
|
||||
@@ -480,6 +482,7 @@
|
||||
<string name="follow_set_creation_item_label">Skapa ny %1$s-lista med användare</string>
|
||||
<string name="follow_set_creation_item_description">Skapar ett %1$s-följ-set och lägger till %2$s i det.</string>
|
||||
<string name="follow_set_creation_dialog_title">Ny följelista</string>
|
||||
<string name="follow_pack_creation_dialog_title">Nytt rekommendationspaket</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopiera/Klona följelista</string>
|
||||
<string name="follow_set_desc_modify_label">Ändra beskrivning</string>
|
||||
<string name="follow_set_empty_desc_label">Den här listan har ingen beskrivning</string>
|
||||
@@ -492,6 +495,7 @@
|
||||
<string name="follow_set_creation_action_btn_label">Skapa lista</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopiera/Klona lista</string>
|
||||
<string name="follow_set_rename_btn_label">Byt namn på lista</string>
|
||||
<string name="follow_set_edit_list_metadata">Redigera lista</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Ändra</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Du byter namn från </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> till..</string>
|
||||
@@ -629,6 +633,7 @@
|
||||
<string name="followed_tags">Följda taggar</string>
|
||||
<string name="relay_setup">Reläer</string>
|
||||
<string name="discover_follows">Följpaket</string>
|
||||
<string name="discover_follows_explainer">Detta är listor med användare som du rekommenderar till andra. Endast offentliga användare är tillåtna.</string>
|
||||
<string name="discover_reads">Läsningar</string>
|
||||
<string name="discover_content_v2">Feed algoritmer</string>
|
||||
<string name="discover_marketplace">Marknadsplats</string>
|
||||
@@ -1120,4 +1125,17 @@
|
||||
<string name="remove_user_from_the_list">Ta bort användare från listan</string>
|
||||
<string name="follow_list_item_label">Följpaket</string>
|
||||
<string name="members">Medlemmar</string>
|
||||
<string name="people_list_title">Metadata för följlista</string>
|
||||
<string name="people_list_explainer">Metadata för följlistor kan ses av alla på Nostr. Endast privata medlemmar är krypterade.</string>
|
||||
<string name="follow_pack_title">Metadata för rekommendationspaket</string>
|
||||
<string name="follow_pack_explainer">Metadata för rekommendationspaket kan ses av alla på Nostr och publiceras ofta på webbplatser som startpaket för nya användare.</string>
|
||||
<string name="follow_pack_edit_list_metadata">Redigera rekommendationspaket</string>
|
||||
<string name="follow_pack_creation_name_label">Paketnamn</string>
|
||||
<string name="follow_pack_copy_name_label">Nytt paketnamn</string>
|
||||
<string name="follow_pack_creation_desc_label">Paketbeskrivning</string>
|
||||
<string name="follow_pack_copy_desc_label">Ny paketbeskrivning</string>
|
||||
<string name="follow_set_broadcast">Sänd lista</string>
|
||||
<string name="follow_pack_broadcast">Sänd rekommendationspaket</string>
|
||||
<string name="follow_set_delete">Ta bort lista</string>
|
||||
<string name="follow_pack_delete">Ta bort rekommendationspaket</string>
|
||||
</resources>
|
||||
|
||||
@@ -444,11 +444,13 @@
|
||||
<string name="follow_list_selection">关注列表</string>
|
||||
<string name="follow_list_kind3follows">所有关注</string>
|
||||
<string name="follow_list_kind3follows_users_only">所有用户关注</string>
|
||||
<string name="follow_list_kind3_follows_users_only">默认关注列表</string>
|
||||
<string name="follow_list_kind3follows_proxy">通过代理关注</string>
|
||||
<string name="follow_list_aroundme">周围的人</string>
|
||||
<string name="follow_list_global">全球</string>
|
||||
<string name="follow_list_mute_list">静音列表</string>
|
||||
<string name="follow_sets">关注集</string>
|
||||
<string name="follow_sets_explainer">这些是为您自己使用设计的关注列表。您可以私下或公开关注用户</string>
|
||||
<string name="labeled_bookmarks">有标签的书签</string>
|
||||
<string name="general_bookmarks">常规书签</string>
|
||||
<string name="follow_set_type_public">公开</string>
|
||||
@@ -483,6 +485,7 @@
|
||||
<string name="follow_set_creation_item_label">创建用户新的 %1$s 列表</string>
|
||||
<string name="follow_set_creation_item_description">创建 %1$s 关注集,并添加 %2$s 到其中。</string>
|
||||
<string name="follow_set_creation_dialog_title">新的 %1$s 列表</string>
|
||||
<string name="follow_pack_creation_dialog_title">新的关注包</string>
|
||||
<string name="follow_set_copy_dialog_title">复制/克隆关注集</string>
|
||||
<string name="follow_set_desc_modify_label">修改描述</string>
|
||||
<string name="follow_set_empty_desc_label">此列表没有描述</string>
|
||||
@@ -494,7 +497,8 @@
|
||||
<string name="follow_set_copy_desc_label">(原始集合描述)</string>
|
||||
<string name="follow_set_creation_action_btn_label">新建集</string>
|
||||
<string name="follow_set_copy_action_btn_label">复制/克隆集</string>
|
||||
<string name="follow_set_rename_btn_label">重命名集</string>
|
||||
<string name="follow_set_rename_btn_label">重命名列表</string>
|
||||
<string name="follow_set_edit_list_metadata">编辑列表</string>
|
||||
<string name="follow_set_desc_modify_btn_label">修改</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">正将集合名称从 </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> 改为</string>
|
||||
@@ -633,6 +637,7 @@
|
||||
<string name="followed_tags">已关注的标签</string>
|
||||
<string name="relay_setup">中继器</string>
|
||||
<string name="discover_follows">关注包</string>
|
||||
<string name="discover_follows_explainer">这些是您推荐给其他人的用户列表。只允许公开用户。</string>
|
||||
<string name="discover_reads">次浏览</string>
|
||||
<string name="discover_content_v2">动态源算法</string>
|
||||
<string name="discover_marketplace">市场</string>
|
||||
@@ -1119,5 +1124,23 @@
|
||||
<string name="in_the_list">已经在列表中</string>
|
||||
<string name="private_members">私密成员</string>
|
||||
<string name="public_members">公开成员</string>
|
||||
<string name="private_members_count">私密成员(%1$s)</string>
|
||||
<string name="public_members_count">公开成员 (%1$s)</string>
|
||||
<string name="add_user_to_the_list">添加用户到列表</string>
|
||||
<string name="remove_user_from_the_list">从列表中删除用户</string>
|
||||
<string name="follow_list_item_label">关注包</string>
|
||||
<string name="members">成员</string>
|
||||
<string name="people_list_title">关注列表元数据</string>
|
||||
<string name="people_list_explainer">关注列表元数据可以被Nostr上的任何人看到。只有您的私人成员是加密的。</string>
|
||||
<string name="follow_pack_title">关注包元数据</string>
|
||||
<string name="follow_pack_explainer">关注包元数据可以被Nostr上的任何人看见,并且常作为新用户启动包发布在许多网站上。</string>
|
||||
<string name="follow_pack_edit_list_metadata">编辑关注包</string>
|
||||
<string name="follow_pack_creation_name_label">包名</string>
|
||||
<string name="follow_pack_copy_name_label">新的包名称</string>
|
||||
<string name="follow_pack_creation_desc_label">包说明</string>
|
||||
<string name="follow_pack_copy_desc_label">新的包说明</string>
|
||||
<string name="follow_set_broadcast">广播列表</string>
|
||||
<string name="follow_pack_broadcast">广播包</string>
|
||||
<string name="follow_set_delete">删除列表</string>
|
||||
<string name="follow_pack_delete">删除包</string>
|
||||
</resources>
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
<string name="report_dialog_title">Block and Report</string>
|
||||
<string name="block_only">Block</string>
|
||||
|
||||
<string name="manual_zaps">Manual Zap Splits</string>
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
<string name="drafts">Drafts</string>
|
||||
<string name="private_bookmarks">Private Bookmarks</string>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.37.3"
|
||||
activityCompose = "1.11.0"
|
||||
agp = "8.13.0"
|
||||
agp = "8.13.1"
|
||||
android-compileSdk = "36"
|
||||
android-minSdk = "26"
|
||||
android-targetSdk = "36"
|
||||
@@ -13,7 +13,7 @@ benchmark = "1.4.1"
|
||||
benchmarkJunit4 = "1.4.1"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coil = "3.3.0"
|
||||
composeBom = "2025.10.01"
|
||||
composeBom = "2025.11.00"
|
||||
coreKtx = "1.17.0"
|
||||
datastore = "1.1.7"
|
||||
espressoCore = "3.7.0"
|
||||
@@ -38,7 +38,7 @@ markdown = "f92ef49c9d"
|
||||
media3 = "1.8.0"
|
||||
mockk = "1.14.6"
|
||||
kotlinx-coroutines-test = "1.10.2"
|
||||
navigationCompose = "2.9.5"
|
||||
navigationCompose = "2.9.6"
|
||||
okhttp = "5.3.0"
|
||||
runner = "1.7.0"
|
||||
rfc3986 = "0.1.2"
|
||||
@@ -49,10 +49,10 @@ torAndroid = "0.4.8.19"
|
||||
translate = "17.0.3"
|
||||
unifiedpush = "3.0.10"
|
||||
urlDetector = "0.1.23"
|
||||
vico-charts = "2.3.0"
|
||||
vico-charts = "2.3.4"
|
||||
zelory = "3.0.1"
|
||||
zoomable = "2.8.2"
|
||||
zxing = "3.5.3"
|
||||
zoomable = "2.9.0"
|
||||
zxing = "3.5.4"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
windowCoreAndroid = "1.5.0"
|
||||
androidxCamera = "1.5.1"
|
||||
|
||||
@@ -255,7 +255,7 @@ mavenPublishing {
|
||||
coordinates(
|
||||
groupId = "com.vitorpamplona.quartz",
|
||||
artifactId = "quartz",
|
||||
version = "1.03.0"
|
||||
version = "1.04.2"
|
||||
)
|
||||
|
||||
// Configure publishing to Maven Central
|
||||
|
||||
+6
-3
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.client
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
@@ -32,7 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface INostrClient {
|
||||
fun relayStatusFlow(): StateFlow<RelayPool.RelayPoolStatus>
|
||||
fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun connect()
|
||||
|
||||
@@ -80,7 +81,9 @@ interface INostrClient {
|
||||
}
|
||||
|
||||
object EmptyNostrClient : INostrClient {
|
||||
override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus())
|
||||
override fun connectedRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
|
||||
|
||||
override fun availableRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
|
||||
|
||||
override fun connect() { }
|
||||
|
||||
|
||||
+3
-1
@@ -320,5 +320,7 @@ class NostrClient(
|
||||
|
||||
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId)
|
||||
|
||||
override fun relayStatusFlow() = relayPool.statusFlow
|
||||
override fun connectedRelaysFlow() = relayPool.connectedRelays
|
||||
|
||||
override fun availableRelaysFlow() = relayPool.availableRelays
|
||||
}
|
||||
|
||||
+28
-52
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.pool
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
@@ -31,8 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* RelayPool manages a collection of Nostr relays, abstracting individual connections and providing
|
||||
@@ -61,9 +60,11 @@ class RelayPool(
|
||||
) : IRelayClientListener {
|
||||
private val relays = LargeCache<NormalizedRelayUrl, IRelayClient>()
|
||||
|
||||
// Backing property to avoid flow emissions from other classes
|
||||
private val _statusFlow = MutableStateFlow<RelayPoolStatus>(RelayPoolStatus())
|
||||
val statusFlow: StateFlow<RelayPoolStatus> = _statusFlow.asStateFlow()
|
||||
private val _connectedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||
val connectedRelays = _connectedRelays.asStateFlow()
|
||||
|
||||
private val _availableRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||
val availableRelays = _availableRelays.asStateFlow()
|
||||
|
||||
fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url)
|
||||
|
||||
@@ -87,21 +88,18 @@ class RelayPool(
|
||||
relay.connectAndSyncFiltersIfDisconnected(ignoreRetryDelays)
|
||||
}
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected()
|
||||
@@ -110,7 +108,6 @@ class RelayPool(
|
||||
relays.forEach { url, relay ->
|
||||
relay.disconnect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun sendOrConnectAndSync(
|
||||
@@ -144,7 +141,13 @@ class RelayPool(
|
||||
// --------------------
|
||||
// Pool Maintenance
|
||||
// --------------------
|
||||
fun getOrCreateRelay(relay: NormalizedRelayUrl) = relays.getOrCreate(relay, ::createNewRelay)
|
||||
fun getOrCreateRelay(relay: NormalizedRelayUrl): IRelayClient {
|
||||
val r = relays.getOrCreate(relay, ::createNewRelay)
|
||||
if (_availableRelays.value.size != relays.size()) {
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
fun createRelayIfAbsent(relay: NormalizedRelayUrl): Boolean = relays.createIfAbsent(relay, ::createNewRelay)
|
||||
|
||||
@@ -155,39 +158,40 @@ class RelayPool(
|
||||
val toRemove = relays.keys() - newRelays
|
||||
var atLeastOne = false
|
||||
|
||||
newRelays.forEach {
|
||||
if (createRelayIfAbsent(it)) {
|
||||
newRelays.forEach { relay ->
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.forEach {
|
||||
if (removeRelayInner(it)) {
|
||||
toRemove.forEach { relay ->
|
||||
if (removeRelayInner(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: NormalizedRelayUrl): IRelayClient {
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
return getOrCreateRelay(relay)
|
||||
}
|
||||
|
||||
fun addAllRelays(relayList: List<NormalizedRelayUrl>) {
|
||||
var atLeastOne = false
|
||||
relayList.forEach {
|
||||
if (createRelayIfAbsent(it)) {
|
||||
relayList.forEach { relay ->
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +206,7 @@ class RelayPool(
|
||||
|
||||
fun removeRelay(relay: NormalizedRelayUrl) {
|
||||
if (removeRelayInner(relay)) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ class RelayPool(
|
||||
if (relays.size() > 0) {
|
||||
disconnect()
|
||||
relays.clear()
|
||||
updateStatus()
|
||||
_availableRelays.update { emptySet() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,12 +228,12 @@ class RelayPool(
|
||||
pingMillis: Int,
|
||||
compressed: Boolean,
|
||||
) {
|
||||
updateStatus()
|
||||
_connectedRelays.update { it + relay.url }
|
||||
listener.onConnected(relay, pingMillis, compressed)
|
||||
}
|
||||
|
||||
override fun onDisconnected(relay: IRelayClient) {
|
||||
updateStatus()
|
||||
_connectedRelays.update { it - relay.url }
|
||||
listener.onDisconnected(relay)
|
||||
}
|
||||
|
||||
@@ -251,33 +255,5 @@ class RelayPool(
|
||||
success: Boolean,
|
||||
) = listener.onSent(relay, cmdStr, cmd, success)
|
||||
|
||||
// ---------------
|
||||
// STATUS Reports
|
||||
// ---------------
|
||||
|
||||
fun availableRelays(): Set<NormalizedRelayUrl> = relays.keys()
|
||||
|
||||
fun connectedRelays(): Set<NormalizedRelayUrl> =
|
||||
relays.mapNotNullIntoSet { url, relay ->
|
||||
if (relay.isConnected()) {
|
||||
url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val connected = connectedRelays()
|
||||
val available = availableRelays()
|
||||
if (_statusFlow.value.connected != connected || _statusFlow.value.available != available) {
|
||||
_statusFlow.tryEmit(RelayPoolStatus(connected, available))
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class RelayPoolStatus(
|
||||
val connected: Set<NormalizedRelayUrl> = emptySet(),
|
||||
val available: Set<NormalizedRelayUrl> = emptySet(),
|
||||
val isConnected: Boolean = connected.isNotEmpty(),
|
||||
)
|
||||
fun connectedRelaysCount(): Int = relays.count { url, relay -> relay.isConnected() }
|
||||
}
|
||||
|
||||
@@ -89,10 +89,7 @@ object Nip19Parser {
|
||||
if (uri == null) return null
|
||||
|
||||
try {
|
||||
val matcher = nip19PlusNip46regex.find(uri)
|
||||
if (matcher == null) {
|
||||
return null
|
||||
}
|
||||
val matcher = nip19PlusNip46regex.find(uri) ?: return null
|
||||
|
||||
val type = matcher.groups[3]?.value ?: matcher.groups[5]?.value // npub1
|
||||
val key = matcher.groups[4]?.value ?: matcher.groups[6]?.value // bech32
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip04DecryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<DecryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseDecrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext))
|
||||
}
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip04EncryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<EncryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseEncrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip44DecryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<DecryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseDecrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip44EncryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<EncryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseEncrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.req
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
class NostrSignerRemote(
|
||||
val signer: NostrSignerInternal,
|
||||
val remotePubkey: HexKey,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val client: INostrClient,
|
||||
val permissions: String? = null,
|
||||
val secret: String? = null,
|
||||
) : NostrSigner(signer.pubKey) {
|
||||
private val manager =
|
||||
RemoteSignerManager(
|
||||
signer = signer,
|
||||
remoteKey = remotePubkey,
|
||||
relayList = relays,
|
||||
client = client,
|
||||
)
|
||||
|
||||
val subscription =
|
||||
client.req(
|
||||
relays = relays.toList(),
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(NostrConnectEvent.KIND),
|
||||
tags = mapOf("p" to listOf(signer.pubKey)),
|
||||
),
|
||||
) { event ->
|
||||
manager.newResponse(event)
|
||||
}
|
||||
|
||||
fun openSubscription() {
|
||||
subscription.updateFilter()
|
||||
}
|
||||
|
||||
fun closeSubscription() {
|
||||
subscription.close()
|
||||
}
|
||||
|
||||
override fun isWriteable(): Boolean = true
|
||||
|
||||
override suspend fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
val template =
|
||||
EventTemplate<Event>(
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = content,
|
||||
)
|
||||
|
||||
BunkerRequestSign(
|
||||
event = template,
|
||||
)
|
||||
},
|
||||
parser = SignResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<SignResult>) {
|
||||
(result.result.event as? T)?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not sign", result)
|
||||
}
|
||||
|
||||
override suspend fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip04Encrypt(
|
||||
message = plaintext,
|
||||
pubKey = toPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip04EncryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<EncryptionResult>) {
|
||||
return result.result.ciphertext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not encrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip04Decrypt(
|
||||
ciphertext = ciphertext,
|
||||
pubKey = fromPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip04DecryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<DecryptionResult>) {
|
||||
return result.result.plaintext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not decrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip44Encrypt(
|
||||
message = plaintext,
|
||||
pubKey = toPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip44EncryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<EncryptionResult>) {
|
||||
return result.result.ciphertext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not encrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip44Decrypt(
|
||||
ciphertext = ciphertext,
|
||||
pubKey = fromPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip44DecryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<DecryptionResult>) {
|
||||
return result.result.plaintext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not decrypt", result)
|
||||
}
|
||||
|
||||
suspend fun ping(): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestPing()
|
||||
},
|
||||
parser = PingResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PingResult>) {
|
||||
return result.result.pong
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not ping", result)
|
||||
}
|
||||
|
||||
suspend fun connect(): HexKey {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestConnect(
|
||||
remoteKey = remotePubkey,
|
||||
permissions = permissions,
|
||||
secret = secret,
|
||||
)
|
||||
},
|
||||
parser = PubKeyResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PublicKeyResult>) {
|
||||
return result.result.pubkey
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not connect", result)
|
||||
}
|
||||
|
||||
suspend fun getPublicKey(): HexKey {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestGetPublicKey()
|
||||
},
|
||||
parser = PubKeyResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PublicKeyResult>) {
|
||||
return result.result.pubkey
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not get public key", result)
|
||||
}
|
||||
|
||||
override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun deriveKey(nonce: HexKey): HexKey {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasForegroundSupport(): Boolean = true
|
||||
|
||||
fun convertExceptions(
|
||||
title: String,
|
||||
result: SignerResult.RequestAddressed<*>,
|
||||
): Exception =
|
||||
when (result) {
|
||||
is SignerResult.RequestAddressed.Successful<*> -> IllegalStateException("$title: This should not happen. There is a bug on Quartz.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult<*> -> IllegalStateException("$title: Failed to parse event: ${result.eventJson}.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent<*> -> IllegalStateException("$title: Failed to verify event: ${result.invalidEvent.toJson()}.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotPerform<*> -> SignerExceptions.CouldNotPerformException("$title: ${result.message}")
|
||||
is SignerResult.RequestAddressed.Rejected<*> -> SignerExceptions.ManuallyUnauthorizedException("$title: User has rejected the request.")
|
||||
is SignerResult.RequestAddressed.TimedOut<*> -> SignerExceptions.TimedOutException("$title: User didn't accept or reject in time.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromBunkerUri(
|
||||
bunkerUri: String,
|
||||
signer: NostrSignerInternal,
|
||||
client: INostrClient,
|
||||
permissions: String? = null,
|
||||
): NostrSignerRemote {
|
||||
if (!bunkerUri.startsWith("bunker://")) throw Exception("Invalid bunker uri")
|
||||
val splitData = bunkerUri.split("?")
|
||||
val remotePubkey = splitData[0].removePrefix("bunker://")
|
||||
if (!Hex.isHex(remotePubkey)) throw Exception("Invalid pubkey in bunker uri")
|
||||
val params = splitData[1].split("&")
|
||||
val relays = mutableSetOf<NormalizedRelayUrl>()
|
||||
var secret: String? = null
|
||||
for (param in params) {
|
||||
val splitParam = param.split("=")
|
||||
if (splitParam.size < 2) continue
|
||||
if (splitParam.first() == "relay") {
|
||||
relays.add(NormalizedRelayUrl(splitParam[1]))
|
||||
}
|
||||
if (splitParam.first() == "secret") {
|
||||
secret = splitParam[1]
|
||||
}
|
||||
}
|
||||
return NostrSignerRemote(
|
||||
signer = signer,
|
||||
remotePubkey = remotePubkey,
|
||||
relays = relays,
|
||||
client = client,
|
||||
permissions = permissions,
|
||||
secret = secret,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
|
||||
|
||||
class PingResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<PingResult> =
|
||||
when (response) {
|
||||
is BunkerResponsePong -> {
|
||||
SignerResult.RequestAddressed.Successful(PingResult(response.id))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
|
||||
|
||||
class PubKeyResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<PublicKeyResult> =
|
||||
when (response) {
|
||||
is BunkerResponsePublicKey -> {
|
||||
SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class RemoteSignerManager(
|
||||
val timeout: Long = 30000,
|
||||
val client: INostrClient,
|
||||
val signer: NostrSignerInternal,
|
||||
val remoteKey: String,
|
||||
val relayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
private val awaitingRequests = LargeCache<String, Continuation<BunkerResponse>>()
|
||||
|
||||
fun newResponse(responseEvent: Event) {
|
||||
val bunkerResponse = OptimizedJsonMapper.fromJsonTo<BunkerResponse>(responseEvent.content)
|
||||
awaitingRequests.get(bunkerResponse.id)?.resume(bunkerResponse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the signer, waits and parses the result
|
||||
*
|
||||
* @param bunkerRequestBuilder The BunkerRequest to be sent.
|
||||
* @param parser A function that parses the BunkerResponse into a [SignerResult.RequestAddressed<T>].
|
||||
* @return The result after parsing the BunkerResponse using the provided parser.
|
||||
*
|
||||
* This function uses the [tryAndWait] utility to implement a timeout on the Bunker Request approval.
|
||||
* It assigns a unique ID to the request and keeps a continuation to resume once the result is received.
|
||||
* If the timeout occurs or the continuation is cancelled, the request ID is cleaned up from [awaitingRequests].
|
||||
*/
|
||||
suspend fun <T : IResult> launchWaitAndParse(
|
||||
bunkerRequestBuilder: () -> BunkerRequest,
|
||||
parser: (response: BunkerResponse) -> SignerResult.RequestAddressed<T>,
|
||||
): SignerResult.RequestAddressed<T> {
|
||||
val request = bunkerRequestBuilder()
|
||||
val event =
|
||||
NostrConnectEvent.create(
|
||||
message = request,
|
||||
remoteKey = remoteKey,
|
||||
signer = signer,
|
||||
)
|
||||
val result =
|
||||
tryAndWait(timeout) { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
awaitingRequests.remove(request.id)
|
||||
}
|
||||
|
||||
awaitingRequests.put(request.id, continuation)
|
||||
|
||||
client.send(event, relayList = relayList)
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
null -> SignerResult.RequestAddressed.TimedOut()
|
||||
else -> parser(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent
|
||||
|
||||
class SignResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<SignResult> =
|
||||
if (response is BunkerResponseEvent) {
|
||||
if (!response.event.verify()) {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(response.event)
|
||||
} else {
|
||||
SignerResult.RequestAddressed.Successful(SignResult(response.event))
|
||||
}
|
||||
} else if (response is BunkerResponseError) {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
} else {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
sealed interface SignerResult<T : IResult> {
|
||||
sealed interface RequestAddressed<T : IResult> : SignerResult<T> {
|
||||
class Successful<T : IResult>(
|
||||
val result: T,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class Rejected<T : IResult> : RequestAddressed<T>
|
||||
|
||||
class TimedOut<T : IResult> : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotPerform<T : IResult>(
|
||||
val message: String? = null,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
|
||||
val eventJson: String,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
|
||||
val invalidEvent: Event,
|
||||
) : RequestAddressed<T>
|
||||
}
|
||||
}
|
||||
|
||||
interface IResult
|
||||
|
||||
data class SignResult(
|
||||
val event: Event,
|
||||
) : IResult
|
||||
|
||||
data class EncryptionResult(
|
||||
val ciphertext: String,
|
||||
) : IResult
|
||||
|
||||
data class DecryptionResult(
|
||||
val plaintext: String,
|
||||
) : IResult
|
||||
|
||||
data class PingResult(
|
||||
val pong: String,
|
||||
) : IResult
|
||||
|
||||
data class PublicKeyResult(
|
||||
val pubkey: String,
|
||||
) : IResult
|
||||
@@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.utils.startsWith
|
||||
import com.vitorpamplona.quartz.utils.startsWithAny
|
||||
import com.vitorpamplona.quartz.utils.startsWithIgnoreCase
|
||||
|
||||
inline fun TagArray.filterToArray(predicate: (Array<String>) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray()
|
||||
|
||||
@@ -31,6 +32,8 @@ inline fun TagArray.remove(predicate: (Array<String>) -> Boolean): TagArray = fi
|
||||
|
||||
fun TagArray.remove(startsWith: Array<String>): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray()
|
||||
|
||||
fun TagArray.removeIgnoreCase(startsWith: Array<String>): TagArray = filterNotTo(ArrayList(this.size), { it.startsWithIgnoreCase(startsWith) }).toTypedArray()
|
||||
|
||||
fun <R> TagArray.removeParsing(
|
||||
transform: (Tag) -> R,
|
||||
equalsTo: R,
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@ import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeIgnoreCase
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@@ -140,8 +140,8 @@ class HashtagListEvent(
|
||||
): HashtagListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(HashtagTag.assemble(hashtag)),
|
||||
tags = earlierVersion.tags.remove(HashtagTag.assemble(hashtag)),
|
||||
privateTags = privateTags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
|
||||
tags = earlierVersion.tags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
@@ -42,18 +42,14 @@ class IMetaTag(
|
||||
const val TAG_NAME = "imeta"
|
||||
const val ANCHOR_PROPERTY = "url"
|
||||
|
||||
fun parse(tag: Array<String>): IMetaTag? {
|
||||
fun parse(tag: Array<String>): List<IMetaTag>? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
val allTags = parseIMeta(tag)
|
||||
val url = allTags[ANCHOR_PROPERTY]?.firstOrNull()
|
||||
|
||||
return if (url != null) {
|
||||
IMetaTag(url, allTags.minus(ANCHOR_PROPERTY))
|
||||
} else {
|
||||
null
|
||||
return allTags[ANCHOR_PROPERTY]?.map {
|
||||
IMetaTag(it, allTags.minus(ANCHOR_PROPERTY))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ package com.vitorpamplona.quartz.nip92IMeta
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.imetas() = this.mapNotNull(IMetaTag::parse)
|
||||
fun TagArray.imetas() = this.mapNotNull(IMetaTag::parse).flatten()
|
||||
|
||||
fun TagArray.imetasByUrl() = this.imetas().associateBy { it.url }
|
||||
|
||||
@@ -40,6 +40,14 @@ fun Array<String>.startsWith(startsWith: Array<String>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun Array<String>.startsWithIgnoreCase(startsWith: Array<String>): Boolean {
|
||||
if (startsWith.size > this.size) return false
|
||||
for (tagIdx in startsWith.indices) {
|
||||
if (!startsWith[tagIdx].equals(this[tagIdx], ignoreCase = true)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun Array<String>.startsWithAny(startsWithList: List<Array<String>>): Boolean = startsWithList.any { startsWith(it) }
|
||||
|
||||
public inline fun <T, R> Array<out T>.lastNotNullOfOrNull(transform: (T) -> R?): R? {
|
||||
|
||||
@@ -147,6 +147,18 @@ class NIP19ParserTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nAddrIncompleteParser() {
|
||||
val result =
|
||||
Nip19Parser.uriToRoute(
|
||||
"nostr:naddr1qqqqzxthwden5te0wfjkccte9ejxjanfdejjuanfv3jk7tczyrv4428upmlcujyf2fy4hqrynywj07ukakr99ufvmmw95n5tttj5qqcyqqqgt0qeresua",
|
||||
)
|
||||
assertEquals(
|
||||
"34236:d95aa8fc0eff8e488952495b8064991d27fb96ed8652f12cdedc5a4e8b5ae540:",
|
||||
(result?.entity as? NAddress)?.aTag(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nAddrParser2() {
|
||||
val result =
|
||||
|
||||
+10
-13
@@ -1,16 +1,13 @@
|
||||
amethyst:
|
||||
android:
|
||||
name: Amethyst
|
||||
description: The all-in-one Nostr client
|
||||
license: MIT
|
||||
builder: npub142gywvjkq0dv6nupggyn2euhx4nduwc7yz5f24ah9rpmunr2s39se3xrj0
|
||||
repository: https://github.com/vitorpamplona/amethyst
|
||||
artifacts:
|
||||
- amethyst-googleplay-arm64-v8a-v%v.apk
|
||||
- amethyst-googleplay-armeabi-v7a-v%v.apk
|
||||
- amethyst-googleplay-universal-v%v.apk
|
||||
- amethyst-googleplay-x86-v%v.apk
|
||||
- amethyst-googleplay-x86_64-v%v.apk
|
||||
name: Amethyst
|
||||
description: The all-in-one Nostr client
|
||||
license: MIT
|
||||
builder: npub142gywvjkq0dv6nupggyn2euhx4nduwc7yz5f24ah9rpmunr2s39se3xrj0
|
||||
repository: https://github.com/vitorpamplona/amethyst
|
||||
homepage: https://amethyst.social/
|
||||
assets:
|
||||
- amethyst-googleplay.*.apk
|
||||
remote_metadata:
|
||||
- github
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user