diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6b4a1500d9..bb096174d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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>(5) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt index be39a1ad0e..27f2b47f85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt @@ -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 = emptySet() + @Volatile private var liveInboxTargets: Set = emptySet() + @Volatile private var liveDmTargets: Set = 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt index 4abfd4345a..7fe5e290a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt @@ -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, -) { +private fun ActivityLogCard(completions: List) { 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,