Compare commits

..
16 Commits
Author SHA1 Message Date
Vitor Pamplona c215beeb8e v0.83.4 2023-12-27 17:38:34 -05:00
Vitor Pamplona 20b730318f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-27 17:00:48 -05:00
Vitor Pamplona abef3d13f2 Improves the design of the discovery cards for Live Activity and Chats. 2023-12-27 17:00:41 -05:00
Vitor Pamplona 77eb066362 Fixes the loading of reactions and zaps to replaceable events. 2023-12-27 17:00:08 -05:00
Vitor Pamplona ec4981852b Fixing the zap of live streams to go to the Host and not the pubkey in the event. 2023-12-27 16:59:28 -05:00
Vitor Pamplona 56c2c60c58 reorganizing Event Factory. 2023-12-27 16:58:56 -05:00
Vitor Pamplona 48aa9f950d adds support for NIP-75 events 2023-12-27 16:58:43 -05:00
Vitor PamplonaandGitHub 37ae06347f Merge pull request #735 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-27 11:49:01 -05:00
Crowdin Bot 78616c07b1 New Crowdin translations by GitHub Action 2023-12-27 16:25:05 +00:00
Vitor Pamplona 73eff8a6c3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-27 11:23:06 -05:00
Vitor Pamplona 4afb0027fb Initial support for separate vertical and horizontal videos. 2023-12-26 18:02:13 -05:00
Vitor Pamplona 1cedc8a6c8 Fixes reconnecting issue when DataSources were not active and become active later. 2023-12-26 18:01:31 -05:00
Vitor PamplonaandGitHub 216ee59fae Merge pull request #732 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-26 15:22:19 -05:00
Crowdin Bot adc4599adf New Crowdin translations by GitHub Action 2023-12-26 19:47:28 +00:00
Vitor Pamplona 8ed960530c Updates dependencies. 2023-12-26 14:43:01 -05:00
Vitor Pamplona 8f13a76870 v0.83.3 2023-12-23 17:45:56 -05:00
25 changed files with 829 additions and 154 deletions
+5 -5
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 343
versionName "0.83.2"
versionCode 345
versionName "0.83.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -89,7 +89,7 @@ android {
buildTypes {
release {
signingConfig signingConfigs.debug
//signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
minifyEnabled true
resValue "string", "app_name", "@string/app_name_release"
@@ -188,7 +188,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Zoomable images
implementation 'net.engawapg.lib:zoomable:1.5.2'
implementation 'net.engawapg.lib:zoomable:1.5.3'
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
@@ -248,7 +248,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.2'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.6.0')
playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
//PushNotifications(FDroid)
@@ -30,7 +30,7 @@ abstract class NostrDataSource(val debugName: String) {
fun printCounter() {
eventCounter.forEach {
Log.d("STATE DUMP", "Received Events ${it.key}: ${it.value.counter}")
Log.d("STATE DUMP ${this.javaClass.simpleName}", "Received Events ${it.key}: ${it.value.counter}")
}
}
@@ -45,6 +45,8 @@ abstract class NostrDataSource(val debugName: String) {
eventCounter = eventCounter + Pair(key, Counter(1))
}
// Log.d(this@NostrDataSource.javaClass.simpleName, "Relay ${relay.url}: ${event.kind}")
consume(event, relay)
if (afterEOSE) {
markAsEOSE(subscriptionId, relay)
@@ -53,16 +55,18 @@ abstract class NostrDataSource(val debugName: String) {
}
override fun onError(error: Error, subscriptionId: String, relay: Relay) {
// Log.e("ERROR", "Relay ${relay.url}: ${error.message}")
// if (subscriptions.containsKey(subscriptionId)) {
// Log.e(
// this@NostrDataSource.javaClass.simpleName,
// "Relay OnError ${relay.url}: ${error.message}"
// )
// }
}
override fun onRelayStateChange(type: Relay.StateType, relay: Relay, subscriptionId: String?) {
// Log.d("RELAY", "Relay ${relay.url} ${when (type) {
// Relay.Type.CONNECT -> "connected."
// Relay.Type.DISCONNECT -> "disconnected."
// Relay.Type.DISCONNECTING -> "disconnecting."
// Relay.Type.EOSE -> "sent all events it had stored."
// }}")
// if (subscriptions.containsKey(subscriptionId)) {
// Log.d(this@NostrDataSource.javaClass.simpleName, "Relay ${relay.url} ${subscriptionId} ${type.name}")
// }
if (type == Relay.StateType.EOSE && subscriptionId != null && subscriptions.containsKey(subscriptionId)) {
markAsEOSE(subscriptionId, relay)
@@ -88,12 +92,13 @@ abstract class NostrDataSource(val debugName: String) {
}
init {
Log.d("Init", "${this.javaClass.simpleName} Subscribe")
Log.d(this.javaClass.simpleName, "${this.javaClass.simpleName} Subscribe")
Client.subscribe(clientListener)
}
fun destroy() {
// makes sure to run
Log.d(this.javaClass.simpleName, "${this.javaClass.simpleName} Unsubscribe")
stop()
Client.unsubscribe(clientListener)
scope.cancel()
@@ -177,32 +182,44 @@ abstract class NostrDataSource(val debugName: String) {
subscriptions.values.forEach { updatedSubscription ->
val updatedSubscriptionNewFilters = updatedSubscription.typedFilters
if (currentFilters.containsKey(updatedSubscription.id)) {
if (updatedSubscriptionNewFilters == null) {
// was active and is not active anymore, just close.
Client.close(updatedSubscription.id)
} else {
// was active and is still active, check if it has changed.
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
Client.close(updatedSubscription.id)
if (active) {
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
}
} else {
// hasn't changed, does nothing.
if (active) {
Client.sendFilterOnlyIfDisconnected(updatedSubscription.id, updatedSubscriptionNewFilters)
}
}
val isActive = Client.isActive(updatedSubscription.id)
if (!isActive && updatedSubscriptionNewFilters != null) {
// Filter was removed from the active list
if (active) {
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
}
} else {
if (updatedSubscriptionNewFilters == null) {
// was not active and is still not active, does nothing
if (currentFilters.containsKey(updatedSubscription.id)) {
if (updatedSubscriptionNewFilters == null) {
// was active and is not active anymore, just close.
Client.close(updatedSubscription.id)
} else {
// was active and is still active, check if it has changed.
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
Client.close(updatedSubscription.id)
if (active) {
Log.d(this@NostrDataSource.javaClass.simpleName, "Update Filter 1 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}")
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
}
} else {
// hasn't changed, does nothing.
if (active) {
Log.d(this@NostrDataSource.javaClass.simpleName, "Update Filter 2 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}")
Client.sendFilterOnlyIfDisconnected(updatedSubscription.id, updatedSubscriptionNewFilters)
}
}
}
} else {
// was not active and becomes active, sends the filter.
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
if (active) {
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
if (updatedSubscriptionNewFilters == null) {
// was not active and is still not active, does nothing
} else {
// was not active and becomes active, sends the filter.
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
if (active) {
Log.d(this@NostrDataSource.javaClass.simpleName, "Update Filter 3 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}")
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
}
}
}
}
@@ -22,13 +22,17 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private var addressesToWatch = setOf<Note>()
private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch.filter { it.address() != null }
val addressesToWatch =
(
eventsToWatch.filter { it.address() != null } +
addressesToWatch.filter { it.address() != null }
).toSet()
if (addressesToWatch.isEmpty()) {
return null
}
return groupByEOSEPresence(eventsToWatch).mapNotNull {
return groupByEOSEPresence(addressesToWatch).map {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ZapSplitSetup
@@ -41,8 +42,14 @@ class ZapPaymentHandler(val account: Account) {
) = withContext(Dispatchers.IO) {
val zapSplitSetup = note.event?.zapSplitSetup()
val noteEvent = note.event
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
noteEvent.hosts().map {
ZapSplitSetup(it, null, weight = 1.0, false)
}
} else {
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
@@ -86,7 +86,7 @@ object Client : RelayPool.Listener {
checkNotInMainThread()
subscriptions = subscriptions + Pair(subscriptionId, filters)
RelayPool.sendFilterOnlyIfDisconnected()
RelayPool.sendFilterOnlyIfDisconnected(subscriptionId)
}
fun send(
@@ -152,6 +152,10 @@ object Client : RelayPool.Listener {
subscriptions = subscriptions.minus(subscriptionId)
}
fun isActive(subscriptionId: String): Boolean {
return subscriptions.contains(subscriptionId)
}
@OptIn(DelicateCoroutinesApi::class)
override fun onEvent(event: Event, subscriptionId: String, relay: Relay, afterEOSE: Boolean) {
// Releases the Web thread for the new payload.
@@ -209,6 +213,10 @@ object Client : RelayPool.Listener {
listeners = listeners.plus(listener)
}
fun isSubscribed(listener: Listener): Boolean {
return listeners.contains(listener)
}
fun unsubscribe(listener: Listener) {
listeners = listeners.minus(listener)
}
@@ -290,7 +290,7 @@ class Relay(
if (filters.isNotEmpty()) {
val request = filters.joinToStringLimited(
separator = ",",
limit = 10,
limit = 20,
prefix = """["REQ","$requestId",""",
postfix = "]"
) { it.filter.toJson(url) }
@@ -339,7 +339,7 @@ class Relay(
return buffer.toString()
}
fun sendFilterOnlyIfDisconnected() {
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
checkNotInMainThread()
if (socket == null) {
@@ -60,8 +60,8 @@ object RelayPool : Relay.Listener {
relays.forEach { it.sendFilter(subscriptionId) }
}
fun sendFilterOnlyIfDisconnected() {
relays.forEach { it.sendFilterOnlyIfDisconnected() }
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
relays.forEach { it.sendFilterOnlyIfDisconnected(subscriptionId) }
}
fun sendToSelectedRelays(list: List<Relay>, signedEvent: EventInterface) {
@@ -0,0 +1,139 @@
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.LikeIcon
import com.vitorpamplona.amethyst.ui.note.TextCount
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@Preview
fun LeftPictureLayoutPreview() {
ThemeComparison(
onDark = { LeftPictureLayoutPreviewCard() },
onLight = { LeftPictureLayoutPreviewCard() }
)
}
@Composable
fun LeftPictureLayoutPreviewCard() {
LeftPictureLayout(
onImage = {
Image(
painter = painterResource(R.drawable.github),
contentDescription = stringResource(id = R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxSize()
.clip(QuoteBorder)
)
},
onTitleRow = {
Text(
text = "This is my title",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeIcon(
iconSizeModifier = Size16Modifier,
grayTint = MaterialTheme.colorScheme.onSurface
)
TextCount(12, MaterialTheme.colorScheme.onSurface)
Spacer(modifier = StdHorzSpacer)
ZappedIcon(Size20Modifier)
TextCount(120, MaterialTheme.colorScheme.onSurface)
},
onDescription = {
Text(
"This is 3-line description, This is 3-line description, This is 3-line description, This is 3-line description",
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
)
},
onBottomRow = {
Text("This is my Moderator List")
}
)
}
@Composable
fun LeftPictureLayout(
onImage: @Composable () -> Unit,
onTitleRow: @Composable RowScope.() -> Unit,
onDescription: @Composable () -> Unit,
onBottomRow: @Composable RowScope.() -> Unit
) {
Row(Modifier.aspectRatio(ratio = 4f)) {
Column(
modifier = Modifier
.fillMaxWidth(0.25f)
.aspectRatio(ratio = 1f)
) {
onImage()
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
onTitleRow()
}
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
onDescription()
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
onBottomRow()
}
}
}
}
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -32,7 +31,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
@@ -54,6 +52,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
@@ -64,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
@@ -689,12 +687,8 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
)
)
Row(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth(0.3f)
.aspectRatio(ratio = 1f)
) {
LeftPictureLayout(
onImage = {
card.cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
@@ -711,29 +705,22 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
DisplayAuthorBanner(it)
}
}
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
}
},
onTitleRow = {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
},
onDescription = {
card.description?.let {
Spacer(modifier = StdVertSpacer)
Row() {
@@ -746,17 +733,16 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
)
}
}
},
onBottomRow = {
Spacer(modifier = StdVertSpacer)
LoadModerators(card.moderators, baseNote, accountViewModel) { participantUsers ->
if (participantUsers.isNotEmpty()) {
Spacer(modifier = StdVertSpacer)
Row(modifier = Modifier.fillMaxWidth()) {
Gallery(participantUsers, accountViewModel)
}
Gallery(participantUsers, accountViewModel)
}
}
}
}
)
}
@Composable
@@ -901,12 +887,8 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
}
}
Row(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth(0.3f)
.aspectRatio(ratio = 1f)
) {
LeftPictureLayout(
onImage = {
cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
@@ -923,43 +905,33 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
DisplayAuthorBanner(it)
}
}
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
}
},
onTitleRow = {
Text(
text = name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
},
onDescription = {
description?.let {
Spacer(modifier = StdVertSpacer)
Row() {
Text(
text = it,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
)
}
Text(
text = it,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
)
}
},
onBottomRow = {
if (participantUsers.isNotEmpty()) {
Spacer(modifier = StdVertSpacer)
Row() {
@@ -967,7 +939,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
}
}
}
}
)
}
@OptIn(ExperimentalLayoutApi::class)
@@ -980,7 +952,7 @@ fun Gallery(users: ImmutableList<User>, accountViewModel: AccountViewModel) {
if (users.size > 6) {
Text(
text = remember(users) { " + " + (showCount(users.size - 6)).toString() },
text = remember(users) { " + " + (showCount(users.size - 6)) },
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface
)
@@ -594,7 +594,7 @@ val slideAnimation: ContentTransform =
)
@Composable
private fun TextCount(count: Int, textColor: Color) {
fun TextCount(count: Int, textColor: Color) {
Text(
text = showCount(count),
fontSize = Font14SP,
+6
View File
@@ -581,4 +581,10 @@
<string name="classifieds_category_misc">Různé</string>
<string name="classifieds_category_other">Ostatní</string>
<string name="failed_to_upload_media_no_details">Nepodařilo se nahrát média</string>
<string name="could_not_open_the_compressed_file">Nepodařilo se otevřít komprimovaný soubor</string>
<string name="error_when_compressing_media">Chyba při komprimování média: %1$s</string>
<string name="failed_to_upload_media">Chyba při nahrávání: %1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">Server po nahrání neposkytl URL</string>
<string name="could_not_download_from_the_server">Nepodařilo se stáhnout nahraná média ze serveru</string>
<string name="could_not_prepare_local_file_to_upload">Nelze připravit místní soubor k nahrání: %1$s</string>
</resources>
+7
View File
@@ -584,4 +584,11 @@ anz der Bedingungen ist erforderlich</string>
<string name="classifieds_category_food">Lebensmittel</string>
<string name="classifieds_category_misc">Verschiedenes</string>
<string name="classifieds_category_other">Sonstiges</string>
<string name="failed_to_upload_media_no_details">Fehler beim Laden der Medien</string>
<string name="could_not_open_the_compressed_file">Konnte die komprimierte Datei nicht öffnen</string>
<string name="error_when_compressing_media">Fehler beim komprimieren der Medien: %1$s</string>
<string name="failed_to_upload_media">Fehler beim Hochladen: %1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">Der Server hat nach dem Hochladen keine URL angegeben</string>
<string name="could_not_download_from_the_server">Hochgeladene Medien konnten nicht vom Server heruntergeladen werden</string>
<string name="could_not_prepare_local_file_to_upload">Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s</string>
</resources>
@@ -580,4 +580,11 @@
<string name="classifieds_category_food">Alimentos</string>
<string name="classifieds_category_misc">Diversos</string>
<string name="classifieds_category_other">Outros</string>
<string name="failed_to_upload_media_no_details">Não foi possível carregar a multimídia</string>
<string name="could_not_open_the_compressed_file">Não foi possível abrir o arquivo compactado</string>
<string name="error_when_compressing_media">Erro ao comprimir a mídia: %1$s</string>
<string name="failed_to_upload_media">Erro ao carregar: %1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">O servidor não forneceu uma URL após o upload</string>
<string name="could_not_download_from_the_server">Não foi possível baixar o arquivo de mídia carregado do servidor</string>
<string name="could_not_prepare_local_file_to_upload">Não foi possível preparar o arquivo local para enviar: %1$s</string>
</resources>
+145 -24
View File
@@ -11,38 +11,45 @@
<string name="referenced_event_not_found">Связанное событие не найдено</string>
<string name="could_not_decrypt_the_message">Не удалось расшифровать сообщение</string>
<string name="group_picture">Фото группы</string>
<string name="explicit_content">Откровенное содержание</string>
<string name="explicit_content">Запрещённый контент</string>
<string name="spam">Спам</string>
<string name="impersonation">Выдача себя за другое лицо</string>
<string name="illegal_behavior">Незаконные действия</string>
<string name="unknown">Неизвестно</string>
<string name="relay_icon">Иконка релея</string>
<string name="unknown_author">Неизвестный автор</string>
<string name="copy_text">Скопировать текст</string>
<string name="copy_user_pubkey">Скопировать ключ пользователя</string>
<string name="copy_note_id">Скопировать ID записи</string>
<string name="copy_text">Копировать текст</string>
<string name="copy_user_pubkey">Копировать ID автора</string>
<string name="copy_note_id">Копировать ID записи</string>
<string name="broadcast">Разослать</string>
<string name="request_deletion">Запросить удаление</string>
<string name="block_hide_user"><![CDATA[Заблокировать и скрыть пользователя]]></string>
<string name="block_report">Блок / Жалоба</string>
<string name="block_hide_user"><![CDATA[Блокировать и скрыть пользователя]]></string>
<string name="report_spam_scam">Сообщить о спаме / мошенничестве</string>
<string name="report_impersonation">Сообщить о выдаче себя за другое лицо</string>
<string name="report_explicit_content">Сообщить об откровенном содержании</string>
<string name="report_impersonation">Сообщить о выдаче себя за другого</string>
<string name="report_explicit_content">Сообщить о запрещённом контенте</string>
<string name="report_illegal_behaviour">Сообщить о незаконных действиях</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Войдите с приватным ключом чтобы ответить</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Войдите с приватным ключом чтобы продвигать записи</string>
<string name="login_with_a_private_key_to_like_posts">Войдите с приватным ключом чтобы лайкать записи</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы ответить</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы продвигать записи</string>
<string name="login_with_a_private_key_to_like_posts">Вы используете публичный ключ, они - только для чтения. Войдите с приватным ключом, чтобы лайкать посты</string>
<string name="no_zap_amount_setup_long_press_to_change">Не настроены запы. Нажмите и удерживайте для настройки</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Войдите с приватным ключом чтобы запать</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы иметь возможность подписаться</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы иметь возможность отписаться</string>
<string name="login_with_a_private_key_to_be_able_to_hide_word">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы иметь возможность скрыть слово или предложение</string>
<string name="login_with_a_private_key_to_be_able_to_show_word">Вы используете публичный ключ, они - только для чтения. Войдите в систему приватным ключом, чтобы иметь возможность показать слово или предложение</string>
<string name="zaps">Запы</string>
<string name="view_count">Просмотры</string>
<string name="boost">Продвинуть</string>
<string name="quote">Цитировать</string>
<string name="quote">Цитата</string>
<string name="new_amount_in_sats">Новая сумма в sat</string>
<string name="add">Добавить</string>
<string name="replying_to">"в ответ "</string>
<string name="replying_to">"ответ на "</string>
<string name="and">" и "</string>
<string name="in_channel">"в канале "</string>
<string name="profile_banner">Баннер профиля</string>
<string name="payment_successful">Платёж выполнен</string>
<string name="error_parsing_error_message">Ошибка при анализе сообщения об ошибке</string>
<string name="following">" Подписок"</string>
<string name="followers">" Подписчиков"</string>
<string name="profile">Профиль</string>
@@ -122,8 +129,8 @@
<string name="follow">Подписаться</string>
<string name="follow_back">Подписаться в ответ</string>
<string name="unblock">Разблокировать</string>
<string name="copy_user_id">Скопировать ID пользователя</string>
<string name="unblock_user">Разблокировать пользователя</string>
<string name="copy_user_id">Копировать ID пользователя</string>
<string name="unblock_user">Разблокировать</string>
<string name="npub_hex_username">"npub, hex, никнейм "</string>
<string name="clear">Очистить</string>
<string name="app_logo">Логотип приложения</string>
@@ -138,6 +145,7 @@
<string name="login">Войти</string>
<string name="generate_a_new_key">Сгенерировать ключ</string>
<string name="loading_feed">Загрузка ленты</string>
<string name="loading_account">Загрузка аккаунта</string>
<string name="error_loading_replies">"Не удалось загрузить ответы: "</string>
<string name="try_again">Повторить</string>
<string name="feed_is_empty">Лента пуста.</string>
@@ -170,7 +178,7 @@
<string name="profanity_hateful_speech">Брань / Оскорбления</string>
<string name="report_hateful_speech">Сообщить об оскорблениях</string>
<string name="report_nudity_porn">Сообщить о наготе / порно</string>
<string name="others">других</string>
<string name="others">другое</string>
<string name="mark_all_known_as_read">Отметить все известные прочитанными</string>
<string name="mark_all_new_as_read">Отметить все новые прочитанными</string>
<string name="mark_all_as_read">Отметить всё прочитанным</string>
@@ -184,16 +192,18 @@
<string name="secret_key_copied_to_clipboard">Приватный ключ (nsec) скопирован</string>
<string name="copy_my_secret_key">Скопировать мой приватный ключ</string>
<string name="biometric_authentication_failed">Ошибка входа</string>
<string name="biometric_authentication_failed_explainer">Биометрия не смогла подтвердить личность владельца этого телефона</string>
<string name="biometric_authentication_failed_explainer_with_error">Биометрия не смогла подтвердить личность владельца этого телефона. Ошибка: %1$s</string>
<string name="biometric_error">Ошибка</string>
<string name="badge_created_by">"Создан %1$s"</string>
<string name="badge_award_image_for">"Картинка значка-награды %1$s"</string>
<string name="new_badge_award_notif">Вы получили в награду новый значок</string>
<string name="award_granted_to">Значок-награда выдан</string>
<string name="copied_note_text_to_clipboard">Скопировать текст</string>
<string name="award_granted_to">Значок награда выдан</string>
<string name="copied_note_text_to_clipboard">Копировать текст</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">ID (npub) автора скопирован</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">ID записи (note1) скопирован</string>
<string name="select_text_dialog_top">Выделить текст</string>
<string name="private_conversation_notification">"&lt;Не удалось расшифровать сообщение&gt;\n\nВас упомянули в приватном/зашифрованном чате между %1$s и %2$s."</string>
<string name="private_conversation_notification">"&lt;Не удалось расшифровать сообщение!&gt;\n\nВас упомянули в приватном/зашифрованном чате между %1$s и %2$s."</string>
<string name="account_switch_add_account_dialog_title">Добавить аккаунт</string>
<string name="drawer_accounts">Аккаунты</string>
<string name="account_switch_select_account">Выбор аккаунта</string>
@@ -205,9 +215,9 @@
<string name="quick_action_select">Выделить</string>
<string name="quick_action_share_browser_link">Поделиться ссылкой</string>
<string name="quick_action_share">Поделиться</string>
<string name="quick_action_copy_user_id">Скопировать ID автора</string>
<string name="quick_action_copy_note_id">Скопировать ID записи</string>
<string name="quick_action_copy_text">Скопировать текст</string>
<string name="quick_action_copy_user_id">ID автора</string>
<string name="quick_action_copy_note_id">ID записи</string>
<string name="quick_action_copy_text">Копировать текст</string>
<string name="quick_action_delete">Удалить</string>
<string name="quick_action_unfollow">@string/unfollow</string>
<string name="quick_action_follow">@string/follow</string>
@@ -224,17 +234,17 @@
<string name="report_dialog_impersonation">@string/impersonation</string>
<string name="report_dialog_nudity">@string/nudity</string>
<string name="report_dialog_illegal">@string/illegal_behavior</string>
<string name="report_dialog_blocking_a_user">Заблокировать и скрыть пользователя для вас. Ваши записи останутся видны тем, кого вы заблокировали. Заблокированные пользователи перечислены на экране Фильтры безопасности</string>
<string name="report_dialog_blocking_a_user">Блокировать и скрыть пользователя для вас. Ваши записи останутся видны тем, кого вы заблокировали. Блокированные пользователи перечислены на экране «Фильтры безопасности».</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[@string/block_hide_user]]></string>
<string name="report_dialog_report_btn">Пожаловаться</string>
<string name="report_dialog_reminder_public">Все жалобы будут видны публично</string>
<string name="report_dialog_reminder_public">Все жалобы будут публично доступны.</string>
<string name="report_dialog_additional_reason_placeholder">По желанию предоставьте дополнительную информацию</string>
<string name="report_dialog_additional_reason_label">Дополнительная информация</string>
<string name="report_dialog_select_reason_label">Причина</string>
<string name="report_dialog_select_reason_placeholder">Выберите причину…</string>
<string name="report_dialog_post_report_btn">Отправить жалобу</string>
<string name="report_dialog_title">Заблокировать и пожаловаться</string>
<string name="block_only">Заблокировать</string>
<string name="block_only">Блокировать</string>
<string name="bookmarks">Закладки</string>
<string name="private_bookmarks">Личные закладки</string>
<string name="public_bookmarks">Публичные закладки</string>
@@ -260,8 +270,11 @@
<string name="poll_zap_value_min">Минимальный зап</string>
<string name="poll_zap_value_max">Максимальный зап</string>
<string name="poll_consensus_threshold">Консенсус</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Закрытие после</string>
<string name="poll_closing_time_days">дней</string>
<string name="poll_unable_to_vote">Невозможно проголосовать</string>
<string name="poll_is_closed_explainer">Опрос закрыт</string>
<string name="poll_zap_amount">Сумма запа</string>
<string name="one_vote_per_user_on_atomic_votes">В этом опросе можно голосовать только один раз</string>
<string name="looking_for_event">"Поиск события %1$s"</string>
@@ -271,6 +284,7 @@
<string name="custom_zaps_add_a_message_example">Спасибо всем вам за работу!</string>
<string name="lightning_create_and_add_invoice">Создать и Добавить</string>
<string name="poll_author_no_vote">Автор опроса не может голосовать</string>
<string name="hash_verification_info_title">Что это значит?</string>
<string name="hash_verification_passed">Фото не изменилось после публикации</string>
<string name="hash_verification_failed">Фото изменено. Автор мог не заметить изменение</string>
<string name="content_description_add_image">Добавить фото</string>
@@ -293,8 +307,26 @@
<string name="zap_forward_lnAddress">LnAddress или @User</string>
<string name="upload_server_relays_nip95">Ваши релеи (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Файл хранится на ваших релеях. Новый NIP: может не работать в других клиентах</string>
<string name="connect_via_tor_short">Настройка Tor/Orbot</string>
<string name="connect_via_tor">Подключение через настройки Orbot</string>
<string name="do_you_really_want_to_disable_tor_title">Отключиться от Orbot/Tor?</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="follow_list_selection">Подписки</string>
<string name="follow_list_kind3follows">Все подписки</string>
<string name="follow_list_global">Глобально</string>
<string name="connect_through_your_orbot_setup_markdown"> ## Подключение через Tor с помощью Orbot
\n\n1. Установите [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Запустите Orbot
\n3. В Orbot проверьте Socks порт. По умолчанию используется 9050
\n4. При необходимости измените порт в Orbot
\n5. Настройте порт Socks на этом экране
\n6. Нажмите кнопку активации, чтобы использовать Orbot как прокси-сервер
</string>
<string name="orbot_socks_port">Порт Orbot Socks</string>
<string name="invalid_port_number">Неверный порт</string>
<string name="use_orbot">Исп. Orbot</string>
<string name="disconnect_from_your_orbot_setup">Отключить Tor/Orbot</string>
<string name="app_notification_dms_channel_name">Личные сообщения</string>
<string name="app_notification_dms_channel_description">Уведомления о входящих сообщениях</string>
<string name="app_notification_zaps_channel_name">Запы</string>
@@ -302,4 +334,93 @@
<string name="app_notification_zaps_channel_message">%1$s sat</string>
<string name="app_notification_zaps_channel_message_from">От %1$s</string>
<string name="app_notification_zaps_channel_message_for">за %1$s</string>
<string name="reply_notify">Уведомление: </string>
<string name="channel_list_user_or_group_id_demo">npub, nevent или hex</string>
<string name="channel_list_join_channel">Войти</string>
<string name="today">Сегодня</string>
<string name="content_warning_hide_all_sensitive_content">Всегда скрывать чувствительный контент</string>
<string name="content_warning_show_all_sensitive_content">Всегда показывать чувствительный контент</string>
<string name="recommended_apps">Рекомендации: </string>
<string name="filter_spam_from_strangers">Фильтр спама от незнакомцев</string>
<string name="warn_when_posts_have_reports_from_your_follows">Предупреждать, когда есть репорты на подписчиков</string>
<string name="owner">Владелец</string>
<string name="version">Версия</string>
<string name="contact">Контакты</string>
<string name="languages">Языки</string>
<string name="tags">Теги</string>
<string name="message_length">Длина сообщения</string>
<string name="subscriptions">Подписки</string>
<string name="filters">Фильтры</string>
<string name="cashu_copy_token">Копировать токен</string>
<string name="copied_token_to_clipboard">Скопирован в буфер обмена</string>
<string name="live_stream_live_tag">В ЭФИРЕ</string>
<string name="live_stream_offline_tag">ОФЛАЙН</string>
<string name="live_stream_ended_tag">ЗАКОНЧЕН</string>
<string name="live_stream_planned_tag">ПО РАСПИСАНИЮ</string>
<string name="live_stream_is_offline">Трансляция выключена</string>
<string name="live_stream_has_ended">Трансляция закончена</string>
<string name="discover_marketplace">Рынок</string>
<string name="discover_live">Стримы</string>
<string name="discover_community">Сообщества</string>
<string name="discover_chat">Чаты</string>
<string name="groups_no_descriptor">У этой группы нет описания или правил. Поговорите с владельцем, чтобы добавить их</string>
<string name="community_no_descriptor">У этого сообщества нет описания. Поговорите с владельцем, чтобы добавить</string>
<string name="add_sensitive_content_label">Чувствительный контент</string>
<string name="add_sensitive_content_description">Добавляет предупреждение о чувствительном содержимом перед показом</string>
<string name="settings">Настройки</string>
<string name="connectivity_type_always">Всегда</string>
<string name="connectivity_type_wifi_only">Только с Wi-Fi</string>
<string name="connectivity_type_never">Никогда</string>
<string name="system">Как в системе</string>
<string name="light">Светлая</string>
<string name="dark">Тёмная</string>
<string name="application_preferences">Настройки приложения</string>
<string name="language">Язык</string>
<string name="theme">Тема</string>
<string name="automatically_load_images_gifs">Просмотр изображения</string>
<string name="automatically_play_videos">Проигрывание видео</string>
<string name="automatically_show_url_preview">Просмотр URL</string>
<string name="automatically_hide_nav_bars">Иммерсивная прокрутка</string>
<string name="automatically_hide_nav_bars_description">Скрыть панели навигации при прокрутке</string>
<string name="spamming_users">Спамеры</string>
<string name="default_relays">По умолчанию</string>
<string name="messages_create_public_chat">Публичный</string>
<string name="messages_new_message">Приватный</string>
<string name="messages_new_message_subject">Заголовок</string>
<string name="paste_from_clipboard">Вставить с буфера обмена</string>
<string name="language_description">Для интерфейса приложения</string>
<string name="theme_description">Тёмная, светлая или системная тема</string>
<string name="automatically_load_images_gifs_description">Автоматически загружать изображения и GIF</string>
<string name="automatically_play_videos_description">Автоматически воспроизводить видео и GIF</string>
<string name="copy_to_clipboard">Копировать в буфер обмена</string>
<string name="rules">Правила</string>
<string name="status_update">Обновить статус</string>
<string name="active_for_chats">Чаты</string>
<string name="active_for_search">Поиск</string>
<string name="hidden_words">Скрытые слова</string>
<string name="hide_new_word_label">Добавить новое слово или предложение</string>
<string name="automatically_show_profile_picture">Картинка профилей</string>
<string name="automatically_show_profile_picture_description">Показывать картинки профилей</string>
<string name="push_server_title">Push уведомления</string>
<string name="push_server_install_app">Настройки push-уведомлений</string>
<string name="push_server_install_app_description"> Для получения push-уведомлений, установите приложение, которое поддерживает [Unified Push](https://unifiedpush.org/), например [Nfty](https://ntfy.sh/).
После установки выберите приложение, которое хотите использовать, в настройках.
</string>
<string name="thread_title">Ветка</string>
<string name="classifieds_category_clothing">Одежда</string>
<string name="classifieds_category_accessories">Аксессуары</string>
<string name="classifieds_category_electronics">Электроника</string>
<string name="classifieds_category_furniture">Мебель</string>
<string name="classifieds_category_collectibles">Коллекции</string>
<string name="classifieds_category_books">Книги</string>
<string name="classifieds_category_pets">Питомцы</string>
<string name="classifieds_category_sports">Спорт</string>
<string name="classifieds_category_fitness">Фитнес</string>
<string name="classifieds_category_art">Искусство</string>
<string name="classifieds_category_crafts">Ремесло</string>
<string name="classifieds_category_home">Дом</string>
<string name="classifieds_category_office">Офис</string>
<string name="classifieds_category_food">Еда</string>
<string name="classifieds_category_misc">Разное</string>
<string name="classifieds_category_other">Другое</string>
</resources>
@@ -578,4 +578,11 @@
<string name="classifieds_category_food">Mat</string>
<string name="classifieds_category_misc">Diverse</string>
<string name="classifieds_category_other">Annat</string>
<string name="failed_to_upload_media_no_details">Det gick inte att ladda upp media</string>
<string name="could_not_open_the_compressed_file">Kunde inte öppna den komprimerade filen</string>
<string name="error_when_compressing_media">Fel vid komprimering av media: %1$s</string>
<string name="failed_to_upload_media">Fel vid uppladdning: %1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">Servern gav inte en URL efter uppladdning</string>
<string name="could_not_download_from_the_server">Kunde inte ladda ner uppladdade medier från servern</string>
<string name="could_not_prepare_local_file_to_upload">Kunde inte förbereda lokal fil att ladda upp: %1$s</string>
</resources>
+1 -1
View File
@@ -521,7 +521,7 @@
<string name="unable_to_download_relay_document">无法下载中继文件</string>
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">无法从闪电地址集合LNURL\"%1$s\"。请检查用户设置</string>
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s 的接收者闪电服务不可用。它是根据雷电地址\"%2$s\"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">无法解析 %1$s。请检查你是否已连接状态,服务器是否启动,以及闪电地址 %2$s 是否正确</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">无法解析 %1$s。请检查你是否已连接,服务器是否启动,以及闪电地址 %2$s 是否正确</string>
<string name="could_not_fetch_invoice_from">无法从 %1$s 获取发票</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">从闪电地址解析JSON出错。请检查用户的打闪设置</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">在用户闪电地址服务器配置中找不到回调URL</string>
@@ -276,6 +276,7 @@
<string name="poll_closing_time">後關閉</string>
<string name="poll_closing_time_days"></string>
<string name="poll_unable_to_vote">無法投票</string>
<string name="poll_is_closed_explainer">該投票已結束</string>
<string name="poll_zap_amount">打閃金額</string>
<string name="one_vote_per_user_on_atomic_votes">這種投票只允許每個用戶一票</string>
<string name="looking_for_event">"正在查找事件 %1$s"</string>
@@ -421,6 +422,8 @@
<string name="automatically_load_images_gifs">圖像預覽</string>
<string name="automatically_play_videos">視頻播放</string>
<string name="automatically_show_url_preview">鏈接預覽</string>
<string name="automatically_hide_nav_bars">沉浸式捲動</string>
<string name="automatically_hide_nav_bars_description">捲動時隱藏導航欄</string>
<string name="load_image">加載圖像</string>
<string name="spamming_users">垃圾郵件</string>
<string name="muted_button">靜音。點擊取消靜音</string>
@@ -456,6 +459,7 @@
<string name="automatically_play_videos_description">自動播放視頻和 GIF</string>
<string name="automatically_show_url_preview_description">顯示鏈接預覽</string>
<string name="load_image_description">何時加載圖像</string>
<string name="copy_to_clipboard">複製至剪貼簿</string>
<string name="copy_url_to_clipboard">將 URL 複製到剪貼簿</string>
<string name="copy_the_note_id_to_the_clipboard">將筆記 ID 複製到剪貼簿</string>
<string name="created_at">創建於</string>
@@ -481,6 +485,7 @@
<string name="zap_split_explainer">支持的客戶端將會把打閃拆分及轉發到在這裡添加的用戶,而不是你的</string>
<string name="zap_split_search_and_add_user">搜索並添加用戶</string>
<string name="zap_split_search_and_add_user_placeholder">用戶名或顯示名</string>
<string name="missing_lud16">閃電設置缺失</string>
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">用户 %1$s 尚未設置接收聰的閃電地址</string>
<string name="zap_split_weight">百分比</string>
<string name="zap_split_weight_placeholder">25</string>
@@ -492,8 +497,14 @@
<string name="error_opening_external_signer">打開簽署程式時出錯</string>
<string name="error_opening_external_signer_description">找不到簽名應用。檢查應用是否已被預載。</string>
<string name="sign_request_rejected">簽署請求被拒絕了</string>
<string name="sign_request_rejected_description">請確保簽名應用程式已授權此交易</string>
<string name="no_wallet_found_with_error">找不到支付閃電發票的錢包(錯誤:%1$s)。請安裝一個閃電錢包來使用打閃</string>
<string name="no_wallet_found">找不到支付閃電發票的錢包。請安裝一個閃電錢包來使用打閃</string>
<string name="hidden_words">隱藏單詞</string>
<string name="hide_new_word_label">隱藏新單詞或句子</string>
<string name="automatically_show_profile_picture">頭像</string>
<string name="automatically_show_profile_picture_description">顯示頭像圖片</string>
<string name="select_an_option">請選擇一個選項</string>
<string name="error_dialog_pay_invoice_error">無法支付發票</string>
<string name="error_dialog_pay_withdraw_error">無法提款</string>
<string name="error_parsing_nip47_title">無法設置錢包連接</string>
@@ -507,12 +518,20 @@
<string name="cashu_no_wallet_found">系統上沒有找到兼容的 Cashu 錢包</string>
<string name="error_unable_to_fetch_invoice">無法從接收方的伺服器獲取發票</string>
<string name="wallet_connect_pay_invoice_error_error">你的錢包連接提供者返回了以下錯誤:%1$s</string>
<string name="could_not_connect_to_tor">無法連線到 Tor</string>
<string name="unable_to_download_relay_document">無法下載中繼文件</string>
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">無法從閃電地址集合 LNURL “%1$s”。請檢查用戶設置</string>
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s 的接收方閃電服務不可用。它是根據閃電地址“%2$s”計算的。錯誤:%3$s。請檢查伺服器是否已上線,閃電地址是否正確</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">無法解析 %1$s。請檢查你是否已連接,伺服器是否啟動,以及閃電地址 %2$s 是否正確</string>
<string name="could_not_fetch_invoice_from">無法從 %1$s 獲取發票</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">從閃電地址解析 JSON 發生錯誤。請檢查用戶的閃電設置</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">在用戶閃電地址伺服器配置中找不到回調 URL</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup">從閃電地址的發票獲取解析 JSON 發生錯誤。請檢查用戶的閃電設置</string>
<string name="incorrect_invoice_amount_sats_from_it_should_have_been">來自 %2$s 的錯誤的發票金額(%1$s 聰)。應該是 %3$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error">無法在發送打閃前創建閃電發票。接收者閃電錢包發送了以下錯誤:%1$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json">無法在發送打閃前創建閃電發票。在生成的 JSON 中找不到 Element pr。</string>
<string name="read_only_user">只讀用戶</string>
<string name="no_reactions_setup">沒有設置回應</string>
<string name="select_push_server">選擇一個統一推送應用</string>
<string name="push_server_title">推送通知</string>
<string name="push_server_explainer">從已安裝的 UnifiedPush 應用中選擇</string>
@@ -526,6 +545,8 @@
<string name="payment_required_title">來自 %1$s 的消息</string>
<string name="thread_title">主題帖</string>
<string name="send_the_seller_a_message">向賣方發送消息</string>
<string name="hi_seller_is_this_still_available">嗨,%1$s,這個還能買到嗎?</string>
<string name="hi_there_is_this_still_available">你好,這個還能買到嗎?</string>
<string name="classifieds">出售物品</string>
<string name="classifieds_title">標題</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
@@ -563,4 +584,7 @@
<string name="could_not_open_the_compressed_file">無法打開壓縮文件</string>
<string name="error_when_compressing_media">壓縮媒體時出錯:%1$s</string>
<string name="failed_to_upload_media">上傳錯誤:%1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">上傳後伺服器沒有提供 URL</string>
<string name="could_not_download_from_the_server">無法從伺服器下載上傳的媒體</string>
<string name="could_not_prepare_local_file_to_upload">無法準備要上傳的本地文件:%1$s</string>
</resources>
+1 -1
View File
@@ -44,7 +44,7 @@ dependencies {
implementation "com.goterl:lazysodium-android:5.1.0@aar"
implementation 'net.java.dev.jna:jna:5.14.0@aar'
api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0'
api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1'
// immutable collections to avoid recomposition
api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7')
@@ -1,5 +1,6 @@
package com.vitorpamplona.quartz.events
import android.widget.VideoView
import com.vitorpamplona.quartz.encoders.toHexKey
class EventFactory {
@@ -23,8 +24,8 @@ class EventFactory {
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.kind -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.kind -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.kind -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.kind -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarRSVPEvent.kind -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
@@ -45,10 +46,8 @@ class EventFactory {
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.kind -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
@@ -56,10 +55,11 @@ class EventFactory {
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.kind -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
GoalEvent.kind -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.kind -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
@@ -79,8 +79,12 @@ class EventFactory {
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig)
StatusEvent.kind -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
VideoHorizontalEvent.kind -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
VideoVerticalEvent.kind -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
VideoViewEvent.kind -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
else -> Event(id, pubKey, createdAt, kind, tags, content, sig)
}
}
@@ -0,0 +1,62 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
@Immutable
class GoalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 9041
const val alt = "Zap Goal"
private const val SUMMARY = "summary"
private const val CLOSED_AT = "closed_at"
private const val IMAGE = "image"
private const val AMOUNT = "amount"
fun create(
description: String,
amount: Long,
relays: Set<String>,
closedAt: Long? = null,
image: String? = null,
summary: String? = null,
websiteUrl: String? = null,
linkedEvent: Event? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (GoalEvent) -> Unit
) {
var tags = mutableListOf(
arrayOf(AMOUNT, amount.toString()),
arrayOf("relays") + relays,
arrayOf("alt", alt)
)
if (linkedEvent is AddressableEvent) {
tags.add(arrayOf("a", linkedEvent.address().toTag()))
} else if (linkedEvent is Event) {
tags.add(arrayOf("e", linkedEvent.id))
}
closedAt?.let { tags.add(arrayOf(CLOSED_AT, it.toString())) }
summary?.let { tags.add(arrayOf(SUMMARY, it)) }
image?.let { tags.add(arrayOf(IMAGE, it)) }
websiteUrl?.let { tags.add(arrayOf("r", it)) }
signer.sign(createdAt, kind, tags.toTypedArray(), description, onReady)
}
}
}
@@ -29,8 +29,13 @@ class LiveActivitiesEvent(
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
fun hasHost() = tags.any { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }
fun host() = tags.firstOrNull { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }?.get(1)
fun hosts() = tags.filter { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }.map { it[1] }
fun checkStatus(eventStatus: String?): String? {
return if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) {
STATUS_ENDED
@@ -0,0 +1,99 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
@Immutable
abstract class VideoEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
companion object {
private const val URL = "url"
private const val ENCRYPTION_KEY = "aes-256-gcm"
private const val MIME_TYPE = "m"
private const val FILE_SIZE = "size"
private const val DIMENSION = "dim"
private const val HASH = "x"
private const val MAGNET_URI = "magnet"
private const val TORRENT_INFOHASH = "i"
private const val BLUR_HASH = "blurhash"
private const val ORIGINAL_HASH = "ox"
private const val ALT = "alt"
private const val TITLE = "title"
private const val PUBLISHED_AT = "published_at"
private const val SUMMARY = "summary"
private const val DURATION = "duration"
private const val IMAGE = "image"
private const val THUMB = "thumb"
fun create(
kind: Int,
url: String,
magnetUri: String? = null,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
originalHash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
altDescription: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit
) {
val tags = listOfNotNull(
arrayOf(URL, url),
magnetUri?.let { arrayOf(MAGNET_URI, it) },
mimeType?.let { arrayOf(MIME_TYPE, it) },
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", altDescription),
hash?.let { arrayOf(HASH, it) },
size?.let { arrayOf(FILE_SIZE, it) },
dimensions?.let { arrayOf(DIMENSION, it) },
blurhash?.let { arrayOf(BLUR_HASH, it) },
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
magnetURI?.let { arrayOf(MAGNET_URI, it) },
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
sensitiveContent?.let {
if (it) {
arrayOf("content-warning", "")
} else {
null
}
}
)
val content = alt ?: ""
signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady)
}
}
}
@@ -0,0 +1,62 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
@Immutable
class VideoHorizontalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : VideoEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 34235
const val altDescription = "Horizontal Video"
fun create(
url: String,
magnetUri: String? = null,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
originalHash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit
) {
create(
kind,
url,
magnetUri,
mimeType,
alt,
hash,
size,
dimensions,
blurhash,
originalHash,
magnetURI,
torrentInfoHash,
encryptionKey,
sensitiveContent,
altDescription,
signer,
createdAt,
onReady
)
}
}
}
@@ -0,0 +1,61 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
@Immutable
class VideoVerticalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : VideoEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 34236
const val altDescription = "Vertical Video"
fun create(
url: String,
magnetUri: String? = null,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
originalHash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit
) {
create(
kind,
url,
magnetUri,
mimeType,
alt,
hash,
size,
dimensions,
blurhash,
originalHash,
magnetURI,
torrentInfoHash,
encryptionKey,
sensitiveContent,
altDescription,
signer,
createdAt,
onReady
)
}
}
}
@@ -0,0 +1,63 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
@Immutable
class VideoViewEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 34237
fun create(
video: ATag,
signer: NostrSigner,
viewStart: Long?,
viewEnd: Long?,
createdAt: Long = TimeUtils.now(),
onReady: (VideoViewEvent) -> Unit
) {
val tags = mutableListOf<Array<String>>()
val aTag = video.toTag()
tags.add(arrayOf("d", aTag))
tags.add(arrayOf("a", aTag))
if (viewEnd != null)
tags.add(arrayOf("viewed", viewStart?.toString() ?: "0", viewEnd.toString()))
else
tags.add(arrayOf("viewed", viewStart?.toString() ?: "0"))
signer.sign(createdAt, kind, tags.toTypedArray(), "", onReady)
}
fun addViewedTime(
event: VideoViewEvent,
viewStart: Long?,
viewEnd: Long?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (VideoViewEvent) -> Unit
) {
val tags = event.tags.toMutableList()
if (viewEnd != null)
tags.add(arrayOf("viewed", viewStart?.toString() ?: "0", viewEnd.toString()))
else
tags.add(arrayOf("viewed", viewStart?.toString() ?: "0"))
signer.sign(createdAt, kind, tags.toTypedArray(), "", onReady)
}
}
}