mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-04 13:54:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c215beeb8e | ||
|
|
20b730318f | ||
|
|
abef3d13f2 | ||
|
|
77eb066362 | ||
|
|
ec4981852b | ||
|
|
56c2c60c58 | ||
|
|
48aa9f950d | ||
|
|
37ae06347f | ||
|
|
78616c07b1 | ||
|
|
73eff8a6c3 | ||
|
|
4afb0027fb | ||
|
|
1cedc8a6c8 | ||
|
|
216ee59fae | ||
|
|
adc4599adf | ||
|
|
8ed960530c | ||
|
|
8f13a76870 |
+5
-5
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">"<Не удалось расшифровать сообщение>\n\nВас упомянули в приватном/зашифрованном чате между %1$s и %2$s."</string>
|
||||
<string name="private_conversation_notification">"<Не удалось расшифровать сообщение!>\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">(0–100)%</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>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user