Queue synchronization of local profile
This commit is contained in:
@@ -80,7 +80,6 @@ abstract class NostrDao(
|
||||
@Transaction
|
||||
open suspend fun storeNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
logger.i("Store Nostr Event: $nostrEvent")
|
||||
@@ -121,14 +120,6 @@ abstract class NostrDao(
|
||||
nostrEvent = nostrEvent,
|
||||
synchronizationRelayURLs = synchronizationRelayURLs
|
||||
)
|
||||
|
||||
// Save synchronizationResult
|
||||
database.synchronizeNostrEventResultDao().upsert(
|
||||
SynchronizeNostrEventResult(
|
||||
nostrEventId = nostrEvent.id,
|
||||
relayURL = synchronizeNostrEventRequest.relayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun indexNostrEvent(
|
||||
|
||||
@@ -13,4 +13,7 @@ interface SynchronizeNostrEventRequestDao {
|
||||
|
||||
@Upsert
|
||||
fun upsert(synchronizeNostrEventRequest: SynchronizeNostrEventRequest)
|
||||
|
||||
@Upsert
|
||||
fun insert(synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
@@ -33,5 +34,12 @@ data class LocalProfile(
|
||||
parentColumn = "id",
|
||||
entityColumn = "unsignedNostrEventId"
|
||||
)
|
||||
val broadcastNostrEventReceipt: BroadcastNostrEventReceipt?
|
||||
)
|
||||
val broadcastNostrEventReceipt: BroadcastNostrEventReceipt?,
|
||||
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "unsignedNostrEventId"
|
||||
)
|
||||
val synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
|
||||
|
||||
)
|
||||
|
||||
@@ -49,7 +49,8 @@ class DatabaseNostrRepository(
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onError: () -> Unit,
|
||||
onProfileReady: (Profile) -> Unit
|
||||
onProfileReady: (Profile) -> Unit,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
val profileEventTemplate = MetadataEvent.createNew(
|
||||
name = name,
|
||||
@@ -82,6 +83,15 @@ class DatabaseNostrRepository(
|
||||
|
||||
if (unsignedNostrEvent.signedAt != null) {
|
||||
// Submit a SyncNostrEventRequest
|
||||
synchronizationRelayURLs.forEach { relayUrl ->
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
SynchronizeNostrEventRequest(
|
||||
authorPublicKeys = unsignedNostrEvent.pubKey,
|
||||
status = "pending",
|
||||
relayURL = relayUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,11 +181,27 @@ class DatabaseNostrRepository(
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
|
||||
database.nostrDao().storeNostrEvent(
|
||||
nostrEvent,
|
||||
synchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs = synchronizationRelayURLs
|
||||
)
|
||||
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
synchronizeNostrEventRequest.copy(
|
||||
nostrEventId = nostrEvent.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun queueSynchronizeNostrEvent(
|
||||
synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
|
||||
) {
|
||||
// Save synchronizationResult
|
||||
database.synchronizeNostrEventRequestDao().insert(
|
||||
synchronizeNostrEventRequests
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,8 @@ interface NostrRepository {
|
||||
onError: () -> Unit,
|
||||
// onUnsignProfile: (UnsignedNostrEvent) -> Unit,
|
||||
// onUnbroadcastedProfile: (NostrEvent) -> Unit,
|
||||
onProfileReady: (Profile) -> Unit
|
||||
onProfileReady: (Profile) -> Unit,
|
||||
synchronizationRelayURLs: List<String>
|
||||
)
|
||||
|
||||
suspend fun createNewTextNote(
|
||||
@@ -59,6 +60,10 @@ interface NostrRepository {
|
||||
synchronizationRelayURLs: List<String>
|
||||
)
|
||||
|
||||
suspend fun queueSynchronizeNostrEvent(
|
||||
synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
|
||||
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
|
||||
@@ -82,7 +87,8 @@ interface NostrRepository {
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onError: () -> Unit,
|
||||
onProfileReady: (Profile) -> Unit
|
||||
onProfileReady: (Profile) -> Unit,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package ac.aux.compose.ui.composable
|
||||
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import ac.aux.compose.ui.theme.AuxTheme
|
||||
import ac.aux.compose.ui.view.model.UnqueuedProfileSynchronizationViewModel
|
||||
import ac.aux.compose.ui.view.model.WriteNewNoteViewModel
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun UnqueuedProfileSynchronizationScreen(
|
||||
profilePublicKey: String,
|
||||
nostrRepository: NostrRepository
|
||||
) {
|
||||
val unqueuedProfileSynchronizationViewModel: UnqueuedProfileSynchronizationViewModel = viewModel(
|
||||
factory = UnqueuedProfileSynchronizationViewModel.factory(
|
||||
profilePublicKey = profilePublicKey,
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
)
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(
|
||||
10.dp
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "We are searching the internet to find your profile and complete sign in.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "We are looking for your profile on as many relays as possible. Nostr aims to be decentralized by distributing data to multiple nodse.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
LoadingDataIndicator(
|
||||
fillScreen = false
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(true) {
|
||||
unqueuedProfileSynchronizationViewModel.queueSynchronization()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UnqueuedProfileSynchronizationScreenPreview() {
|
||||
AuxTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
UnqueuedProfileSynchronizationScreen(
|
||||
profilePublicKey = "npub is for living."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import ac.aux.compose.ui.composable.navigation.routes.SocialPreconditionRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnannouncedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnindexedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnqueuedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnqueuedProfileSynchronizationRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnsignedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnsyncedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.WriteNewNoteRoute
|
||||
@@ -42,7 +43,6 @@ import ac.aux.compose.ui.view.state.NavigationUIState
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -53,7 +53,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -129,6 +128,15 @@ fun AuxNavHost(
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
is NavigationUIState.UnqueuedProfileSynchronization -> {
|
||||
navController.navigate(
|
||||
route = UnqueuedProfileSynchronizationRoute(
|
||||
state.unsignedNostrEvent.pubKey
|
||||
)
|
||||
) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
is NavigationUIState.UnindexedProfile -> {
|
||||
navController.navigate(
|
||||
route = UnindexedProfileRoute(
|
||||
@@ -243,6 +251,12 @@ fun AuxNavHost(
|
||||
unsyncedProfilePublicKey = route.publicKey
|
||||
)
|
||||
}
|
||||
composable<UnqueuedProfileSynchronizationRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<UnqueuedProfileSynchronizationRoute>()
|
||||
UnqueuedProfileSynchronizationRoute(
|
||||
publicKey = route.publicKey
|
||||
)
|
||||
}
|
||||
composable<UnindexedProfileRoute> {
|
||||
UnindexedProfileScreen()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package ac.aux.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UnqueuedProfileSynchronizationRoute(
|
||||
val publicKey: String,
|
||||
): Route() {
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
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.CreateProfileUIState
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -80,25 +81,13 @@ class CreateProfileViewModel(
|
||||
onError = {
|
||||
createProfileUIState.value = CreateProfileUIState.Error
|
||||
},
|
||||
// onUnsignProfile = { unsignedNostrEvent ->
|
||||
// onNavigateToUnsignedProfile.invoke(
|
||||
// UnsignedProfileRoute(
|
||||
// unsignedNostrEvent.id
|
||||
// )
|
||||
// )
|
||||
// },
|
||||
// onUnbroadcastedProfile = { unannouncedNostrEvent ->
|
||||
// onNavigateToUnannouncedProfile.invoke(
|
||||
// UnannouncedProfileRoute(
|
||||
// nostrEventId = unannouncedNostrEvent.id
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
) { profile ->
|
||||
createProfileUIState.value = CreateProfileUIState.ProfileReady(
|
||||
profile = profile
|
||||
)
|
||||
}
|
||||
{ profile ->
|
||||
createProfileUIState.value = CreateProfileUIState.ProfileReady(
|
||||
profile = profile
|
||||
)
|
||||
},
|
||||
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,16 +193,18 @@ class NavigationViewModel(
|
||||
jsonText
|
||||
)
|
||||
|
||||
val nostrEvent = NostrEvent(
|
||||
id = event.id,
|
||||
pubKey = event.pubKey,
|
||||
kind = event.kind,
|
||||
content = event.content,
|
||||
tags = event.tags,
|
||||
createdAt = Instant.fromEpochMilliseconds(event.createdAt),
|
||||
sig = event.sig
|
||||
)
|
||||
|
||||
nostrRepository.saveNostrEvent(
|
||||
nostrEvent = NostrEvent(
|
||||
id = event.id,
|
||||
pubKey = event.pubKey,
|
||||
kind = event.kind,
|
||||
content = event.content,
|
||||
tags = event.tags,
|
||||
createdAt = Instant.fromEpochMilliseconds(event.createdAt),
|
||||
sig = event.sig
|
||||
),
|
||||
nostrEvent = nostrEvent,
|
||||
synchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
|
||||
)
|
||||
@@ -315,7 +317,12 @@ class NavigationViewModel(
|
||||
NavigationUIState.UnindexedProfile(
|
||||
nostrEvent = localProfile.nostrEvent
|
||||
)
|
||||
} else if (localProfile.unsignedNostrEvent != null && localProfile.unsignedNostrEvent.signedAt != null) {
|
||||
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
|
||||
logger.i("We should request to sync profile: ${localProfile.unsignedNostrEvent}")
|
||||
NavigationUIState.UnqueuedProfileSynchronization(
|
||||
unsignedNostrEvent = localProfile.unsignedNostrEvent
|
||||
)
|
||||
} else if (localProfile.unsignedNostrEvent.signedAt != null) {
|
||||
logger.i("We should be syncing the profile: ${localProfile.unsignedNostrEvent}")
|
||||
NavigationUIState.UnsyncedProfile(
|
||||
unsignedNostrEvent = localProfile.unsignedNostrEvent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
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.SignInToProfileUIState
|
||||
import ac.aux.compose.ui.view.state.form.SignInToProfileFormState
|
||||
@@ -12,7 +12,6 @@ import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
@@ -78,7 +77,8 @@ class SignInToProfileViewModel(
|
||||
name = null,
|
||||
biography = null,
|
||||
onError = onError,
|
||||
onProfileReady = {}
|
||||
onProfileReady = {},
|
||||
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
|
||||
)
|
||||
}
|
||||
is NSec -> {
|
||||
@@ -87,7 +87,8 @@ class SignInToProfileViewModel(
|
||||
name = null,
|
||||
biography = null,
|
||||
onError = onError,
|
||||
onProfileReady = {}
|
||||
onProfileReady = {},
|
||||
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.nostr.Relays
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.view.state.SignInToProfileUIState
|
||||
import ac.aux.compose.ui.view.state.form.SignInToProfileFormState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UnqueuedProfileSynchronizationViewModel(
|
||||
val profilePublicKey: String,
|
||||
val nostrRepository: NostrRepository
|
||||
): ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "UnqueuedProfileSynchronizationViewModel"
|
||||
|
||||
fun factory(
|
||||
profilePublicKey: String,
|
||||
nostrRepository: NostrRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
UnqueuedProfileSynchronizationViewModel(
|
||||
profilePublicKey = profilePublicKey,
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
fun queueSynchronization() {
|
||||
logger.d { "queueSynchronization" }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
nostrRepository.queueSynchronizeNostrEvent(
|
||||
Relays.eventPublishRelaySet.map { normalizedRelayUrl ->
|
||||
SynchronizeNostrEventRequest(
|
||||
authorPublicKeys = profilePublicKey,
|
||||
kinds = "0",
|
||||
relayURL = normalizedRelayUrl.url
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
|
||||
abstract class NavigationUIState {
|
||||
data object Error: NavigationUIState()
|
||||
@@ -22,6 +21,10 @@ abstract class NavigationUIState {
|
||||
val unsignedNostrEvent: UnsignedNostrEvent,
|
||||
) : NavigationUIState()
|
||||
|
||||
data class UnqueuedProfileSynchronization(
|
||||
val unsignedNostrEvent: UnsignedNostrEvent,
|
||||
) : NavigationUIState()
|
||||
|
||||
data class UnindexedProfile(
|
||||
val nostrEvent: NostrEvent,
|
||||
) : NavigationUIState()
|
||||
|
||||
Reference in New Issue
Block a user