Adds a RelaySync filter by since/until dates.
This commit is contained in:
+39
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-10
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-9
@@ -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."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user