renames class and other small changes

This commit is contained in:
Vitor Pamplona
2026-03-17 11:02:30 -04:00
parent aecbcb68a5
commit 45e0dc9bd5
3 changed files with 98 additions and 99 deletions
@@ -79,7 +79,7 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
@@ -176,7 +176,7 @@ class AccountViewModel(
val toastManager = ToastManager()
val broadcastTracker = BroadcastTracker()
val feedStates = AccountFeedContentStates(account, viewModelScope)
val eventSyncViewModel = EventSyncViewModel(account, viewModelScope)
val eventSync = EventSync(account, viewModelScope)
val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5)
@@ -12,12 +12,11 @@
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
@@ -32,11 +31,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
@@ -47,6 +49,7 @@ import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.coroutineContext
/**
* Syncs the user's events across all known relays:
@@ -72,7 +75,7 @@ import java.util.concurrent.atomic.AtomicLong
*
* Scoped to the AccountViewModel so the sync survives navigation within the same session.
*/
class EventSyncViewModel(
class EventSync(
val account: Account,
private val scope: CoroutineScope,
) {
@@ -160,7 +163,9 @@ class EventSyncViewModel(
/** Destination relay sets, set at the start of each sync run. */
@Volatile private var liveOutboxTargets: Set<NormalizedRelayUrl> = emptySet()
@Volatile private var liveInboxTargets: Set<NormalizedRelayUrl> = emptySet()
@Volatile private var liveDmTargets: Set<NormalizedRelayUrl> = emptySet()
private fun emitLiveSnapshot() {
@@ -228,10 +233,13 @@ class EventSyncViewModel(
val startTime = System.currentTimeMillis()
val myPubKey = account.signer.pubKey
val allRelays =
account.cache.relayHints.relayDB
val allRelays = listOf("wss://relay.damus.io".normalizeRelayUrl())
/*
account.cache.relayHints.relayDB
.keys()
.toList()
*/
if (allRelays.isEmpty()) {
_syncState.value =
@@ -317,10 +325,7 @@ class EventSyncViewModel(
totalSent.incrementAndGet()
}
}
val pTagsMe =
event.tags.any { tag ->
tag.size >= 2 && tag[0] == "p" && tag[1] == myPubKey
}
val pTagsMe = event.tags.isTaggedUser(myPubKey)
if (pTagsMe) {
if (event.kind == 4 || event.kind == 1059) {
if (dmTargets.isNotEmpty() && dmSent.add(event.id)) {
@@ -475,7 +480,7 @@ class EventSyncViewModel(
done.close()
val count = pageCount.get()
if (count == 0) break // relay exhausted or unreachable
if (count == 0) break // relay exhausted or unreachable
totalEvents += count
@@ -12,12 +12,11 @@
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
@@ -59,10 +58,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
@@ -72,14 +71,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun EventSyncScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val syncViewModel = accountViewModel.eventSyncViewModel
val syncViewModel = accountViewModel.eventSync
val syncState by syncViewModel.syncState.collectAsStateWithLifecycle()
val liveActivity by syncViewModel.liveActivity.collectAsStateWithLifecycle()
@@ -103,68 +101,66 @@ fun EventSyncScreen(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Spacer(Modifier.height(8.dp))
// ---- Explanation card ----
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringRes(R.string.event_sync_what_happens_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(8.dp))
Text(
text = stringRes(R.string.event_sync_what_happens_body),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(Modifier.height(12.dp))
StepRow(number = "1", text = stringRes(R.string.event_sync_step1))
Spacer(Modifier.height(4.dp))
StepRow(number = "2", text = stringRes(R.string.event_sync_step2))
Spacer(Modifier.height(4.dp))
StepRow(number = "3", text = stringRes(R.string.event_sync_step3))
}
}
// ---- WiFi warning ----
if (isMobileOrMetered) {
Card(
modifier = Modifier.fillMaxWidth(),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
),
) {
Text(
text = stringRes(R.string.event_sync_wifi_warning),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp),
)
}
}
// ---- Progress / Status area ----
when (val state = syncState) {
is EventSyncViewModel.SyncState.Idle -> Unit
is EventSync.SyncState.Idle -> {
// ---- Explanation card ----
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringRes(R.string.event_sync_what_happens_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(8.dp))
Text(
text = stringRes(R.string.event_sync_what_happens_body),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(Modifier.height(12.dp))
StepRow(number = "1", text = stringRes(R.string.event_sync_step1))
Spacer(Modifier.height(4.dp))
StepRow(number = "2", text = stringRes(R.string.event_sync_step2))
Spacer(Modifier.height(4.dp))
StepRow(number = "3", text = stringRes(R.string.event_sync_step3))
}
}
is EventSyncViewModel.SyncState.Running -> {
// ---- WiFi warning ----
if (isMobileOrMetered) {
Card(
modifier = Modifier.fillMaxWidth(),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
),
) {
Text(
text = stringRes(R.string.event_sync_wifi_warning),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp),
)
}
}
}
is EventSync.SyncState.Running -> {
SyncProgressCard(state = state)
}
is EventSyncViewModel.SyncState.Paused -> {
is EventSync.SyncState.Paused -> {
PausedCard(state = state)
}
is EventSyncViewModel.SyncState.Done -> {
is EventSync.SyncState.Done -> {
DoneCard(state = state)
}
is EventSyncViewModel.SyncState.Error -> {
is EventSync.SyncState.Error -> {
ErrorCard(message = state.message)
}
}
@@ -183,9 +179,9 @@ fun EventSyncScreen(
// ---- Action buttons ----
when (val state = syncState) {
is EventSyncViewModel.SyncState.Idle,
is EventSyncViewModel.SyncState.Done,
is EventSyncViewModel.SyncState.Error,
is EventSync.SyncState.Idle,
is EventSync.SyncState.Done,
is EventSync.SyncState.Error,
-> {
Button(
onClick = {
@@ -201,7 +197,7 @@ fun EventSyncScreen(
}
}
is EventSyncViewModel.SyncState.Paused -> {
is EventSync.SyncState.Paused -> {
Button(
onClick = {
if (isMobileOrMetered) {
@@ -222,7 +218,7 @@ fun EventSyncScreen(
}
}
is EventSyncViewModel.SyncState.Running -> {
is EventSync.SyncState.Running -> {
OutlinedButton(
onClick = { syncViewModel.cancel() },
modifier = Modifier.fillMaxWidth(),
@@ -238,7 +234,7 @@ fun EventSyncScreen(
// ---- Mobile-data confirmation dialog ----
if (showMobileDataDialog) {
val isPaused = syncState is EventSyncViewModel.SyncState.Paused
val isPaused = syncState is EventSync.SyncState.Paused
AlertDialog(
onDismissRequest = { showMobileDataDialog = false },
title = { Text(stringRes(R.string.event_sync_mobile_data_dialog_title)) },
@@ -271,7 +267,7 @@ fun EventSyncScreen(
// -------------------------------------------------------------------------
@Composable
private fun SyncProgressCard(state: EventSyncViewModel.SyncState.Running) {
private fun SyncProgressCard(state: EventSync.SyncState.Running) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
@@ -305,7 +301,7 @@ private fun SyncProgressCard(state: EventSyncViewModel.SyncState.Running) {
}
@Composable
private fun PausedCard(state: EventSyncViewModel.SyncState.Paused) {
private fun PausedCard(state: EventSync.SyncState.Paused) {
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -337,7 +333,7 @@ private fun PausedCard(state: EventSyncViewModel.SyncState.Paused) {
}
@Composable
private fun DoneCard(state: EventSyncViewModel.SyncState.Done) {
private fun DoneCard(state: EventSync.SyncState.Done) {
Card(
modifier = Modifier.fillMaxWidth(),
colors =
@@ -409,7 +405,7 @@ private fun ErrorCard(message: String) {
* Shows where events are being sent: outbox, inbox, and DM relay lists.
*/
@Composable
private fun DestinationRelaysCard(activity: EventSyncViewModel.LiveSyncActivity) {
private fun DestinationRelaysCard(activity: EventSync.LiveSyncActivity) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
@@ -500,9 +496,7 @@ private fun DestinationSection(
* Uses a fixed-height inner scroll area so it doesn't compete with the outer scroll.
*/
@Composable
private fun ActivityLogCard(
completions: List<EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo>,
) {
private fun ActivityLogCard(completions: List<EventSync.LiveSyncActivity.CompletedRelayInfo>) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
@@ -539,7 +533,7 @@ private fun ActivityLogCard(
}
@Composable
private fun ActivityLogRow(info: EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo) {
private fun ActivityLogRow(info: EventSync.LiveSyncActivity.CompletedRelayInfo) {
val hasEvents = info.eventsFound > 0
val dotColor =
if (hasEvents) {
@@ -652,16 +646,16 @@ private fun formatCount(n: Int): String =
private val previewCompletions =
listOf(
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://relay.damus.io"), 1247, 891),
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://nos.lol"), 892, 45),
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://relay.nostr.band"), 3500, 3498),
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://slow.relay.example.com"), 0, 0),
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://nostr.bitcoiner.social"), 15, 0),
EventSyncViewModel.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://unreachable.relay.xyz"), 0, 0),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://relay.damus.io"), 1247, 891),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://nos.lol"), 892, 45),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://relay.nostr.band"), 3500, 3498),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://slow.relay.example.com"), 0, 0),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://nostr.bitcoiner.social"), 15, 0),
EventSync.LiveSyncActivity.CompletedRelayInfo(NormalizedRelayUrl("wss://unreachable.relay.xyz"), 0, 0),
)
private val previewActivity =
EventSyncViewModel.LiveSyncActivity(
EventSync.LiveSyncActivity(
recentCompletions = previewCompletions,
outboxTargets =
setOf(
@@ -689,7 +683,7 @@ fun SyncProgressCardPreview() {
ThemeComparisonColumn {
SyncProgressCard(
state =
EventSyncViewModel.SyncState.Running(
EventSync.SyncState.Running(
relaysCompleted = 312,
totalRelays = 1024,
eventsSent = 4821,
@@ -704,7 +698,7 @@ fun PausedCardPreview() {
ThemeComparisonColumn {
PausedCard(
state =
EventSyncViewModel.SyncState.Paused(
EventSync.SyncState.Paused(
nextRelayIndex = 260,
totalRelays = 1024,
eventsSent = 3200,
@@ -719,7 +713,7 @@ fun DoneCardPreview() {
ThemeComparisonColumn {
DoneCard(
state =
EventSyncViewModel.SyncState.Done(
EventSync.SyncState.Done(
totalEventsSent = 18_432,
totalEventsAccepted = 14_891,
durationMs = 187_000,