Adds a RelaySync filter by since/until dates.

This commit is contained in:
Vitor Pamplona
2026-03-22 11:18:28 -04:00
parent b6b4856fe4
commit 40c45fb3a1
4 changed files with 245 additions and 19 deletions
@@ -23,9 +23,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -38,6 +42,7 @@ import org.junit.runner.RunWith
class EventSyncTest { class EventSyncTest {
companion object { companion object {
val vitor = "wss://vitor.nostr1.com".normalizeRelayUrl() val vitor = "wss://vitor.nostr1.com".normalizeRelayUrl()
val fiatjaf = "wss://pyramid.fiatjaf.com".normalizeRelayUrl()
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val rootClient = val rootClient =
@@ -70,4 +75,38 @@ class EventSyncTest {
sync.runSync() sync.runSync()
} }
@Test
fun testFiatjafSync() =
runBlocking {
val sync =
EventSync(
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
relayDb = { listOf(fiatjaf) },
outboxTargets = { setOf(vitor) },
inboxTargets = { setOf(vitor) },
dmTargets = { setOf(vitor) },
clientBuilder = {
val newClient = NostrClient(socketBuilder, appScope)
val logger = RelayLogger(newClient, debugSending = true, debugReceiving = false)
val signer = NostrSignerInternal(KeyPair())
// Authenticates with relays.
val auth =
RelayAuthenticator(
newClient,
appScope,
signWithAllLoggedInUsers = { authTemplate ->
listOf(signer.sign(authTemplate))
},
)
newClient
},
scope = appScope,
)
sync.runSync()
}
} }
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_ACTIVITY_LOG import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_ACTIVITY_LOG
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_CONCURRENT_RELAYS import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_CONCURRENT_RELAYS
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.RELAY_TIMEOUT_MS
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.LiveSyncActivity.SourceRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.LiveSyncActivity.SourceRelayInfo
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -106,14 +107,18 @@ class EventSync(
val eventsSent: MutableStateFlow<Int>, val eventsSent: MutableStateFlow<Int>,
val eventsReceived: MutableStateFlow<Int>, val eventsReceived: MutableStateFlow<Int>,
val eventsAccepted: MutableStateFlow<Int>, val eventsAccepted: MutableStateFlow<Int>,
val sinceFilter: Long? = null,
val untilFilter: Long? = null,
) : SyncState() { ) : SyncState() {
constructor(relaysCompleted: Int, totalRelays: Int, eventsSent: Int, eventsReceived: Int, eventsAccepted: Int) : constructor(relaysCompleted: Int, totalRelays: Int, eventsSent: Int, eventsReceived: Int, eventsAccepted: Int, sinceFilter: Long? = null, untilFilter: Long? = null) :
this( this(
relaysCompleted = MutableStateFlow(relaysCompleted), relaysCompleted = MutableStateFlow(relaysCompleted),
totalRelays = MutableStateFlow(totalRelays), totalRelays = MutableStateFlow(totalRelays),
eventsSent = MutableStateFlow(eventsSent), eventsSent = MutableStateFlow(eventsSent),
eventsReceived = MutableStateFlow(eventsReceived), eventsReceived = MutableStateFlow(eventsReceived),
eventsAccepted = MutableStateFlow(eventsAccepted), eventsAccepted = MutableStateFlow(eventsAccepted),
sinceFilter = sinceFilter,
untilFilter = untilFilter,
) )
} }
@@ -122,10 +127,14 @@ class EventSync(
val totalEventsSent: Int, val totalEventsSent: Int,
val totalEventsAccepted: Int, val totalEventsAccepted: Int,
val durationMs: Long, val durationMs: Long,
val sinceFilter: Long? = null,
val untilFilter: Long? = null,
) : SyncState() ) : SyncState()
data class Error( data class Error(
val message: String, val message: String,
val sinceFilter: Long? = null,
val untilFilter: Long? = null,
) : SyncState() ) : SyncState()
} }
@@ -282,14 +291,20 @@ class EventSync(
// Control functions // Control functions
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/** User-selected date range for the sync filter (epoch seconds). */
val sinceSecs = MutableStateFlow<Long?>(null)
val untilSecs = MutableStateFlow<Long?>(null)
private var syncJob: Job? = null private var syncJob: Job? = null
fun start() { fun start() {
if (_syncState.value is SyncState.Running) return if (_syncState.value is SyncState.Running) return
_liveActivity.value = LiveSyncActivity() _liveActivity.value = LiveSyncActivity()
val capturedSince = sinceSecs.value
val capturedUntil = untilSecs.value
syncJob = syncJob =
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
runSync() runSync(capturedSince, capturedUntil)
} }
} }
@@ -300,7 +315,10 @@ class EventSync(
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Sync logic // Sync logic
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
suspend fun runSync() { suspend fun runSync(
filterSince: Long? = null,
filterUntil: Long? = null,
) {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
_liveActivity.value = LiveSyncActivity() _liveActivity.value = LiveSyncActivity()
@@ -311,7 +329,7 @@ class EventSync(
if (relaysToProcess.isEmpty()) { if (relaysToProcess.isEmpty()) {
_syncState.value = _syncState.value =
SyncState.Error("No known relays found. Browse some content first to discover relays.") SyncState.Error("No known relays found. Browse some content first to discover relays.", filterSince, filterUntil)
return return
} }
@@ -323,14 +341,14 @@ class EventSync(
val defaultFilters = val defaultFilters =
buildList { buildList {
if (outboxTargets.isNotEmpty()) add(Filter(authors = listOf(myPubKey))) if (outboxTargets.isNotEmpty()) add(Filter(authors = listOf(myPubKey), since = filterSince, until = filterUntil))
if (inboxTargets.isNotEmpty() || dmTargets.isNotEmpty()) { if (inboxTargets.isNotEmpty() || dmTargets.isNotEmpty()) {
add(Filter(tags = mapOf("p" to listOf(myPubKey)))) add(Filter(tags = mapOf("p" to listOf(myPubKey)), since = filterSince, until = filterUntil))
} }
} }
if (defaultFilters.isEmpty()) { if (defaultFilters.isEmpty()) {
_syncState.value = SyncState.Error("No outbox, inbox, or DM relays configured.") _syncState.value = SyncState.Error("No outbox, inbox, or DM relays configured.", filterSince, filterUntil)
return return
} }
@@ -342,9 +360,9 @@ class EventSync(
defaultFilters defaultFilters
} else { } else {
buildList { buildList {
if (it !in outboxTargets) add(Filter(authors = listOf(myPubKey))) if (it !in outboxTargets) add(Filter(authors = listOf(myPubKey), since = filterSince, until = filterUntil))
if (it !in inboxTargets && it !in dmTargets) { if (it !in inboxTargets && it !in dmTargets) {
add(Filter(tags = mapOf("p" to listOf(myPubKey)))) add(Filter(tags = mapOf("p" to listOf(myPubKey)), since = filterSince, until = filterUntil))
} }
} }
} }
@@ -373,6 +391,8 @@ class EventSync(
eventsSent = 0, eventsSent = 0,
eventsReceived = 0, eventsReceived = 0,
eventsAccepted = 0, eventsAccepted = 0,
sinceFilter = filterSince,
untilFilter = filterUntil,
) )
val okListener = val okListener =
@@ -563,6 +583,8 @@ class EventSync(
totalEventsSent = runningState.eventsSent.value, totalEventsSent = runningState.eventsSent.value,
totalEventsAccepted = runningState.eventsAccepted.value, totalEventsAccepted = runningState.eventsAccepted.value,
durationMs = System.currentTimeMillis() - startTime, durationMs = System.currentTimeMillis() - startTime,
sinceFilter = filterSince,
untilFilter = filterUntil,
) )
} catch (e: Exception) { } catch (e: Exception) {
_syncState.value = _syncState.value =
@@ -571,10 +593,12 @@ class EventSync(
totalEventsSent = runningState.eventsSent.value, totalEventsSent = runningState.eventsSent.value,
totalEventsAccepted = runningState.eventsAccepted.value, totalEventsAccepted = runningState.eventsAccepted.value,
durationMs = System.currentTimeMillis() - startTime, durationMs = System.currentTimeMillis() - startTime,
sinceFilter = filterSince,
untilFilter = filterUntil,
) )
if (e is CancellationException) throw e if (e is CancellationException) throw e
_syncState.value = SyncState.Error(e.message ?: "Unknown error") _syncState.value = SyncState.Error(e.message ?: "Unknown error", filterSince, filterUntil)
} finally { } finally {
client.unsubscribe(okListener) client.unsubscribe(okListener)
} }
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -43,10 +44,12 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -54,6 +57,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -70,6 +75,9 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable @Composable
fun EventSyncScreen( fun EventSyncScreen(
@@ -94,6 +102,8 @@ fun EventSyncScreen(
syncState = syncState, syncState = syncState,
liveActivity = liveActivity, liveActivity = liveActivity,
isMobileOrMetered = isMobileOrMetered, isMobileOrMetered = isMobileOrMetered,
onSinceChanged = { syncViewModel.sinceSecs.value = it },
onUntilChanged = { syncViewModel.untilSecs.value = it },
onStart = syncViewModel::start, onStart = syncViewModel::start,
onCancel = syncViewModel::cancel, onCancel = syncViewModel::cancel,
) )
@@ -106,6 +116,8 @@ fun EventScreenBody(
syncState: EventSync.SyncState, syncState: EventSync.SyncState,
liveActivity: EventSync.LiveSyncActivity, liveActivity: EventSync.LiveSyncActivity,
isMobileOrMetered: Boolean = false, isMobileOrMetered: Boolean = false,
onSinceChanged: (Long?) -> Unit = {},
onUntilChanged: (Long?) -> Unit = {},
onStart: () -> Unit = {}, onStart: () -> Unit = {},
onCancel: () -> Unit = {}, onCancel: () -> Unit = {},
) { ) {
@@ -119,10 +131,10 @@ fun EventScreenBody(
item { item {
// ---- Progress / Status area ---- // ---- Progress / Status area ----
when (syncState) { when (syncState) {
is EventSync.SyncState.Idle -> ExplanationCard(isMobileOrMetered, onStart) is EventSync.SyncState.Idle -> ExplanationCard(isMobileOrMetered, onSinceChanged, onUntilChanged, onStart)
is EventSync.SyncState.Running -> SyncProgressCard(state = syncState, onCancel) is EventSync.SyncState.Running -> SyncProgressCard(state = syncState, onCancel)
is EventSync.SyncState.Done -> DoneCard(state = syncState, isMobileOrMetered, onStart) is EventSync.SyncState.Done -> DoneCard(state = syncState, isMobileOrMetered, onSinceChanged, onUntilChanged, onStart)
is EventSync.SyncState.Error -> ErrorCard(syncState.message, isMobileOrMetered, onStart) is EventSync.SyncState.Error -> ErrorCard(syncState, isMobileOrMetered, onSinceChanged, onUntilChanged, onStart)
} }
} }
@@ -231,7 +243,9 @@ private fun StartSyncButton(
@Composable @Composable
private fun ExplanationCard( private fun ExplanationCard(
isMobileOrMetered: Boolean, isMobileOrMetered: Boolean,
onStart: () -> Unit, onSinceChanged: (Long?) -> Unit = {},
onUntilChanged: (Long?) -> Unit = {},
onStart: () -> Unit = {},
) { ) {
// ---- Explanation card ---- // ---- Explanation card ----
Card( Card(
@@ -256,6 +270,7 @@ private fun ExplanationCard(
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
StepRow(number = "3", text = stringRes(R.string.event_sync_step3)) StepRow(number = "3", text = stringRes(R.string.event_sync_step3))
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
// ---- WiFi warning ---- // ---- WiFi warning ----
if (isMobileOrMetered) { if (isMobileOrMetered) {
Card( Card(
@@ -274,6 +289,12 @@ private fun ExplanationCard(
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
} }
DateRangeFilterCard(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
onSinceChanged = onSinceChanged,
onUntilChanged = onUntilChanged,
)
Spacer(Modifier.height(14.dp))
StartSyncButton(isMobileOrMetered = isMobileOrMetered, onStart) StartSyncButton(isMobileOrMetered = isMobileOrMetered, onStart)
} }
} }
@@ -352,6 +373,8 @@ private fun RelayStatement(state: EventSync.SyncState.Running) {
private fun DoneCard( private fun DoneCard(
state: EventSync.SyncState.Done, state: EventSync.SyncState.Done,
isMobileOrMetered: Boolean = false, isMobileOrMetered: Boolean = false,
onSinceChanged: (Long?) -> Unit = {},
onUntilChanged: (Long?) -> Unit = {},
onStart: () -> Unit = { }, onStart: () -> Unit = { },
) { ) {
Card( Card(
@@ -387,6 +410,16 @@ private fun DoneCard(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f),
) )
Spacer(Modifier.height(14.dp))
DateRangeFilterCard(
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
onSinceStartValue = state.sinceFilter,
onUntilStartValue = state.untilFilter,
onSinceChanged = onSinceChanged,
onUntilChanged = onUntilChanged,
)
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
StartSyncButton(isMobileOrMetered, onStart) StartSyncButton(isMobileOrMetered, onStart)
} }
@@ -395,8 +428,10 @@ private fun DoneCard(
@Composable @Composable
private fun ErrorCard( private fun ErrorCard(
message: String, state: EventSync.SyncState.Error,
isMobileOrMetered: Boolean = false, isMobileOrMetered: Boolean = false,
onSinceChanged: (Long?) -> Unit = {},
onUntilChanged: (Long?) -> Unit = {},
onStart: () -> Unit = {}, onStart: () -> Unit = {},
) { ) {
Card( Card(
@@ -415,10 +450,19 @@ private fun ErrorCard(
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = message, text = state.message,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer, color = MaterialTheme.colorScheme.onErrorContainer,
) )
Spacer(Modifier.height(14.dp))
DateRangeFilterCard(
backgroundColor = MaterialTheme.colorScheme.errorContainer,
onSinceStartValue = state.sinceFilter,
onUntilStartValue = state.untilFilter,
onSinceChanged = onSinceChanged,
onUntilChanged = onUntilChanged,
)
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
StartSyncButton(isMobileOrMetered, onStart) StartSyncButton(isMobileOrMetered, onStart)
} }
@@ -660,6 +704,117 @@ private fun ActivityLogRow(info: EventSync.LiveSyncActivity.SourceRelayInfo) {
} }
} }
/** Oldest possible "since" — roughly 2020-01-01 in epoch seconds. */
private const val EPOCH_2023 = 1_672_549_201L
@Composable
private fun DateRangeFilterCard(
backgroundColor: Color,
onSinceStartValue: Long? = null,
onUntilStartValue: Long? = null,
onSinceChanged: (Long?) -> Unit = {},
onUntilChanged: (Long?) -> Unit = {},
) {
val now = remember { System.currentTimeMillis() / 1000 }
// Slider range: 0f = EPOCH_2020, 1f = now
val range = (now - EPOCH_2023).toFloat()
// Initialise from lastSyncTimestamp when available, otherwise full range
var sliderValues by remember(onSinceStartValue, onUntilStartValue) {
val initialSince =
if (onSinceStartValue != null) {
((onSinceStartValue - EPOCH_2023) / range).coerceIn(0f, 1f)
} else {
0f
}
val finalSince =
if (onUntilStartValue != null) {
((onUntilStartValue - EPOCH_2023) / range).coerceIn(0f, 1f)
} else {
1f
}
mutableStateOf(initialSince..finalSince)
}
// Derive actual timestamps
val sinceEpoch = (EPOCH_2023 + (sliderValues.start * range).toLong())
val untilEpoch = (EPOCH_2023 + (sliderValues.endInclusive * range).toLong())
// Fire callbacks
val effectiveSince = if (sliderValues.start <= 0.001f) null else sinceEpoch
val effectiveUntil = if (sliderValues.endInclusive >= 0.999f) null else untilEpoch
// Notify parent on each change
LaunchedEffect(effectiveSince) { onSinceChanged(effectiveSince) }
LaunchedEffect(effectiveUntil) { onUntilChanged(effectiveUntil) }
Card(
modifier =
Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = backgroundColor),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringRes(R.string.event_sync_date_filter_title),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
)
Spacer(Modifier.height(12.dp))
// Labels for current selection
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text =
if (effectiveSince == null) {
stringRes(R.string.event_sync_date_filter_all_time)
} else {
stringRes(R.string.event_sync_date_filter_since) + " " + formatEpochDate(sinceEpoch)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
)
Text(
text =
if (effectiveUntil == null) {
stringRes(R.string.event_sync_date_filter_now)
} else {
stringRes(R.string.event_sync_date_filter_until) + " " + formatEpochDate(untilEpoch)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
)
}
Box(
// block left drawer and back gestures when sliding
Modifier.pointerInput(Unit) {
detectDragGestures { change, _ ->
change.consume()
}
},
) {
RangeSlider(
value = sliderValues,
onValueChange = { sliderValues = it },
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
private fun formatEpochDate(epochSecs: Long): String {
val sdf = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
return sdf.format(Date(epochSecs * 1000))
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Helpers // Helpers
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -747,7 +902,7 @@ private val previewActivity =
@Preview @Preview
fun IdleCardWifiPreview() { fun IdleCardWifiPreview() {
ThemeComparisonColumn { ThemeComparisonColumn {
ExplanationCard(false, {}) ExplanationCard(false)
} }
} }
@@ -755,7 +910,7 @@ fun IdleCardWifiPreview() {
@Preview @Preview
fun IdleCardMobilePreview() { fun IdleCardMobilePreview() {
ThemeComparisonColumn { ThemeComparisonColumn {
ExplanationCard(true, {}) ExplanationCard(true)
} }
} }
@@ -797,7 +952,7 @@ fun DoneCardPreview() {
@Preview @Preview
fun ErrorCardPreview() { fun ErrorCardPreview() {
ThemeComparisonColumn { ThemeComparisonColumn {
ErrorCard(message = "No outbox, inbox, or DM relays configured.") ErrorCard(state = EventSync.SyncState.Error("No outbox, inbox, or DM relays configured."))
} }
} }
+8
View File
@@ -1912,4 +1912,12 @@
<string name="attestor_proficiency_for_kinds">Proficient in verifying kinds: %1$s</string> <string name="attestor_proficiency_for_kinds">Proficient in verifying kinds: %1$s</string>
<string name="attestation_attests_to">Attests to</string> <string name="attestation_attests_to">Attests to</string>
<string name="attestation_requests_attestation_to">Requests attestation to</string> <string name="attestation_requests_attestation_to">Requests attestation to</string>
<string name="event_sync_date_filter_title">Date Range</string>
<string name="event_sync_date_filter_since">Since</string>
<string name="event_sync_date_filter_until">Until</string>
<string name="event_sync_date_filter_now">Now</string>
<string name="event_sync_date_filter_all_time">All time</string>
<string name="event_sync_date_filter_last_sync">Last sync: %1$s</string>
<string name="event_sync_date_filter_since_last_sync">Since Last Sync</string>
</resources> </resources>