From b3b93008e53b0d25c489eeeacfd11600482125a6 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Wed, 1 Apr 2026 01:47:24 +0200 Subject: [PATCH] Prep to show loading on pending sync --- .../ac.aux.compose.database.AuxDatabase/1.json | 12 +++++++++--- .../kotlin/ac/aux/compose/database/dao/NostrDao.kt | 7 +++++++ .../dao/SynchronizeNostrEventRequestDao.kt | 3 +++ .../database/model/SynchronizeNostrEventRequest.kt | 1 + .../database/repository/DatabaseNostrRepository.kt | 7 +++++++ .../ac/aux/compose/repository/NostrRepository.kt | 6 ++++++ .../ac/aux/compose/ui/composable/FeedScreen.kt | 14 +++++++++++++- .../aux/compose/ui/view/model/FeedListViewModel.kt | 12 ++++++++++++ .../ui/view/model/NostrEventDetailViewModel.kt | 1 + .../UnqueuedProfileSynchronizationViewModel.kt | 1 + 10 files changed, 60 insertions(+), 4 deletions(-) diff --git a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json index 6431844b..3f2e89d0 100644 --- a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "31032766d6bdd6ae4664e854a928dd32", + "identityHash": "770e86deaca12c1e4d82a71f6613da37", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -929,7 +929,7 @@ }, { "tableName": "SynchronizeNostrEventRequest", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `isRecommendedRelay` INTEGER NOT NULL, `synchronizationFilters` TEXT NOT NULL, `nostrEventId` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`unsignedNostrEventId`) REFERENCES `UnsignedNostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `purpose` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `isRecommendedRelay` INTEGER NOT NULL, `synchronizationFilters` TEXT NOT NULL, `nostrEventId` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`unsignedNostrEventId`) REFERENCES `UnsignedNostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -937,6 +937,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "purpose", + "columnName": "purpose", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "status", "columnName": "status", @@ -1368,7 +1374,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '31032766d6bdd6ae4664e854a928dd32')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '770e86deaca12c1e4d82a71f6613da37')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt index b6872069..26cd9db0 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -122,6 +122,7 @@ abstract class NostrDao( database.synchronizeNostrEventRequestDao().insert( synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( authors = arrayOf(nostrEvent.pubKey), @@ -144,6 +145,7 @@ abstract class NostrDao( val synchronizeNostrEventRequests = if (recommendRelayUrl != null) { listOf( SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( ids = arrayOf(taggedEvent.eventId) @@ -156,6 +158,7 @@ abstract class NostrDao( } else { synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( ids = arrayOf(taggedEvent.eventId) @@ -178,6 +181,7 @@ abstract class NostrDao( val synchronizeNostrEventRequests = if (recommendedRelay != null) { listOf( SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( authors = arrayOf(taggedUser.pubKey), @@ -191,6 +195,7 @@ abstract class NostrDao( } else { synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( authors = arrayOf(taggedUser.pubKey), @@ -266,6 +271,7 @@ abstract class NostrDao( database.synchronizeNostrEventRequestDao().insert( synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( authors = arrayOf(nostrEvent.pubKey), @@ -298,6 +304,7 @@ abstract class NostrDao( database.synchronizeNostrEventRequestDao().insert( synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( + purpose = "synchronization", synchronizationFilters = arrayOf( SynchronizationFilter( ids = arrayOf(zap.postId) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/SynchronizeNostrEventRequestDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/SynchronizeNostrEventRequestDao.kt index 572ea8c4..a2ac0b50 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/SynchronizeNostrEventRequestDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/SynchronizeNostrEventRequestDao.kt @@ -12,6 +12,9 @@ interface SynchronizeNostrEventRequestDao { @Query("SELECT * FROM SynchronizeNostrEventRequest WHERE status = :status") fun observeSynchronizeNostrEventRequestsByStatus(status: String): Flow + @Query("SELECT COUNT(*) FROM SynchronizeNostrEventRequest WHERE purpose = :purpose AND status IN (:status)") + fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String, status: List): Flow + @Upsert fun upsert(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt index 6ee293fa..68f0e1b3 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt @@ -38,6 +38,7 @@ import kotlin.uuid.Uuid data class SynchronizeNostrEventRequest( @PrimaryKey val id: String = Uuid.generateV4().toHexDashString(), + val purpose: String, val status: String = "pending", val relayURL: String, val isRecommendedRelay: Boolean = false, diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index c6ff8123..2a7c15a2 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -297,6 +297,13 @@ class DatabaseNostrRepository( ) } + override suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow { + return database.synchronizeNostrEventRequestDao().observeSynchronizeNostrEventRequestByPurposeAndStatusCount( + "feed", + listOf("pending", "processing") + ) + } + override suspend fun observeNostrFeed(): Flow> { return database.nostrEventDao().observeNostrEvents( kinds = arrayOf( diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index 4bef588b..8e988ebb 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -65,6 +65,8 @@ interface NostrRepository { synchronizeNostrEventRequests: List, ) + suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow + suspend fun observeNostrFeed(): Flow> suspend fun observeLocalNostrEventById(nostrEventId: String): Flow @@ -145,6 +147,10 @@ interface NostrRepository { TODO("Not yet implemented") } + override suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow { + TODO("Not yet implemented") + } + override suspend fun observeNostrFeed(): Flow> { TODO("Not yet implemented") } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt index 5138eeed..10bdace4 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt @@ -11,8 +11,10 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -23,10 +25,12 @@ import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold @@ -45,7 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun FeedScreen( initialFeedListUIState: FeedListUIState, @@ -194,6 +198,14 @@ fun FeedScreen( } } + if (feedListViewModel.isSynchronizationPending.value) { + item { + Spacer( + modifier = Modifier.height(50.dp) + ) + LoadingIndicator() + } + } items( items = feedListUIState.localNostrEvents, key = { localNostrEvent -> localNostrEvent.nostrEvent.id } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt index 9c029e8d..51cc0843 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt @@ -6,6 +6,7 @@ import ac.aux.compose.managers.SeedManager import ac.aux.compose.nostr.Relays import ac.aux.compose.repository.NostrRepository import ac.aux.compose.ui.view.state.FeedListUIState +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -14,6 +15,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -28,9 +30,12 @@ class FeedListViewModel( initialFeedListUIState: FeedListUIState, val nostrRepository: NostrRepository ): ViewModel() { + val logger = Logger.withTag(TAG) var feedListUIState: FeedListUIState by mutableStateOf(initialFeedListUIState) private set + val isSynchronizationPending: MutableState = mutableStateOf(false) + fun retryLoad() { feedListUIState = FeedListUIState.Loading loadNostrFeed() @@ -42,6 +47,7 @@ class FeedListViewModel( nostrRepository.queueSynchronizeNostrEvent( Relays.eventPublishRelaySet.map { normalizedRelay -> SynchronizeNostrEventRequest( + purpose = "feed", synchronizationFilters = arrayOf( SynchronizationFilter( kinds = arrayOf( @@ -61,6 +67,7 @@ class FeedListViewModel( } ) + // Get contact list and sync feed... val localProfile = nostrRepository.observeProfile(SeedManager.activePublicKey().toHexKey()).firstOrNull() @@ -73,6 +80,11 @@ class FeedListViewModel( localNostrEvents = localNostrEvents ) } + + nostrRepository.observeSynchronizeNostrEventRequestByPurposeAndStatusCount("feed").collect { count -> + logger.d("Pending/Processing Synchronization Request: $count") + isSynchronizationPending.value = count > 0 + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt index 3ba40b01..59e9f51e 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt @@ -43,6 +43,7 @@ class NostrEventDetailViewModel( nostrRepository.queueSynchronizeNostrEvent( Relays.eventPublishRelaySet.map { normalizedRelay -> SynchronizeNostrEventRequest( + purpose = "detail", synchronizationFilters = arrayOf( SynchronizationFilter( kinds = arrayOf( diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt index 6f313a3e..0c3ab10c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt @@ -55,6 +55,7 @@ class UnqueuedProfileSynchronizationViewModel( nostrRepository.queueSynchronizeNostrEvent( Relays.eventPublishRelaySet.map { normalizedRelayUrl -> SynchronizeNostrEventRequest( + purpose = "sign-in", synchronizationFilters = arrayOf( SynchronizationFilter( authors = arrayOf(profilePublicKey),