Bug fix on infinite sync

This commit is contained in:
Kgothatso Ngako
2026-03-30 01:17:44 +02:00
parent 6696e8d57b
commit 63c2530075
13 changed files with 272 additions and 147 deletions

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "75e5c80b2c3183c8a4fa300627ffb5db",
"identityHash": "9a3031f0a4cb56c9b5d6c26be0f48a3c",
"entities": [
{
"tableName": "BroadcastNostrEventReceipt",
@@ -873,7 +873,7 @@
},
{
"tableName": "SynchronizeNostrEventRequest",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `eventIds` TEXT, `authorPublicKeys` TEXT, `kinds` TEXT, `tagName` TEXT, `since` INTEGER, `until` INTEGER, `limit` INTEGER, `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, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `eventIds` TEXT, `authorPublicKeys` TEXT, `kinds` TEXT, `tagName` TEXT, `since` INTEGER, `until` INTEGER, `limit` INTEGER, `search` TEXT, `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",
@@ -928,6 +928,11 @@
"columnName": "limit",
"affinity": "INTEGER"
},
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT"
},
{
"fieldPath": "nostrEventId",
"columnName": "nostrEventId",
@@ -1330,7 +1335,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, '75e5c80b2c3183c8a4fa300627ffb5db')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a3031f0a4cb56c9b5d6c26be0f48a3c')"
]
}
}

View File

@@ -1,6 +1,6 @@
package ac.aux.compose.database
import ac.aux.compose.database.converters.Converters
import ac.aux.compose.database.converters.AuxConverters
import ac.aux.compose.database.dao.BroadcastNostrEventReceiptDao
import ac.aux.compose.database.dao.BroadcastNostrEventRequestDao
import ac.aux.compose.database.dao.NostrDao
@@ -46,7 +46,7 @@ import androidx.room.useWriterConnection
],
version = 1
)
@TypeConverters(Converters::class)
@TypeConverters(AuxConverters::class)
abstract class AuxDatabase: RoomDatabase() {
abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao
abstract fun broadcastNostrEventRequestDao(): BroadcastNostrEventRequestDao

View File

@@ -1,11 +1,16 @@
package ac.aux.compose.database.converters
import ac.aux.compose.database.model.typealiases.HexArray
import ac.aux.compose.database.model.typealiases.KindArray
import androidx.room.TypeConverter
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import kotlinx.serialization.json.Json
import kotlin.time.Instant
class Converters {
class AuxConverters {
val logger = Logger.withTag("AuxConverters")
@TypeConverter
fun fromTimestamp(value: Long?): Instant? {
return value?.let { Instant.fromEpochMilliseconds(it) }
@@ -27,4 +32,45 @@ class Converters {
// Decodes the JSON string back into the nested array structure
return value?.let { Json.decodeFromString<TagArray>(it) }
}
@TypeConverter
fun fromHexArray(value: HexArray?): String? = try {
return value?.let {
Json.encodeToString(it).lowercase()
}
} catch (e: Throwable) {
logger.e("Failed to convert from HexArray $value", e)
return null
}
@TypeConverter
fun toHexArray(value: String?): HexArray? = try {
return value?.let {
Json.decodeFromString<HexArray>(it)
}
} catch (e: Throwable) {
logger.e("Failed to convert to HexArray $value", e)
return null
}
@TypeConverter
fun fromKindArray(value: KindArray?): String? = try {
return value?.let {
Json.encodeToString(it)
}
} catch (e: Throwable) {
logger.e("Failed to convert from KindArray $value", e)
return null
}
@TypeConverter
fun toKindArray(value: String?): KindArray? = try {
return value?.let {
Json.decodeFromString<KindArray>(it)
}
} catch (e: Throwable) {
logger.e("Failed to convert to KindArray $value", e)
return null
}
}

View File

@@ -83,36 +83,38 @@ abstract class NostrDao(
synchronizationRelayURLs: List<String>
) {
logger.i("Store Nostr Event: $nostrEvent")
// Find or create profile with the pubKey... if not found submit a sync request...
val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey)
if (nostrEvent.unsignedNostrEventId == null) {
// Find or create profile with the pubKey... if not found submit a sync request...
val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey)
if (profile == null) {
val placeHolderProfile = Profile(
publicKey = nostrEvent.pubKey,
nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information
)
val placeHolderProfileNostrEvent = NostrEvent(
id = placeHolderProfile.publicKey,
kind = 30024,
pubKey = placeHolderProfile.publicKey,
content = placeHolderProfile.publicKey,
createdAt = Clock.System.now(),
sig = placeHolderProfile.publicKey,
tags = emptyArray(),
)
database.nostrEventDao().upsert(placeHolderProfileNostrEvent)
database.profileDao().upsert(placeHolderProfile)
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
authorPublicKeys = listOf<String>(nostrEvent.pubKey).joinToString(","),
relayURL = synchronizationRelayURL
)
if (profile == null) {
val placeHolderProfile = Profile(
publicKey = nostrEvent.pubKey,
nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information
)
val placeHolderProfileNostrEvent = NostrEvent(
id = placeHolderProfile.publicKey,
kind = 30024,
pubKey = placeHolderProfile.publicKey,
content = placeHolderProfile.publicKey,
createdAt = Clock.System.now(),
sig = placeHolderProfile.publicKey,
tags = emptyArray(),
)
database.nostrEventDao().upsert(placeHolderProfileNostrEvent)
database.profileDao().upsert(placeHolderProfile)
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
authorPublicKeys = arrayOf(nostrEvent.pubKey),
kinds = arrayOf(0),
relayURL = synchronizationRelayURL
)
)
}
}
}
database.nostrEventDao().upsert(nostrEvent)
// Index nostrEvent
@@ -146,7 +148,7 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
eventIds = listOf(post.replyToId).joinToString(","),
eventIds = arrayOf(post.replyToId),
relayURL = synchronizationRelayURL
)
)
@@ -171,7 +173,7 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
eventIds = listOf(post.repostId).joinToString(","),
eventIds = arrayOf(post.repostId),
relayURL = synchronizationRelayURL
)
)

View File

@@ -1,9 +1,10 @@
package ac.aux.compose.database.model
import ac.aux.compose.database.model.traits.NostrEventEntity
import ac.aux.compose.database.model.traits.OptionalNostrEventEntity
import ac.aux.compose.database.model.traits.TimestampedEntity
import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity
import ac.aux.compose.database.model.typealiases.HexArray
import ac.aux.compose.database.model.typealiases.KindArray
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@@ -40,16 +41,59 @@ data class SynchronizeNostrEventRequest(
val id: String = Uuid.generateV4().toHexDashString(),
val status: String = "pending",
val relayURL: String,
val eventIds: String? = null, // TODO: EventIdArray
val authorPublicKeys: String? = null, // TODO: PublicKeyArray
val kinds: String? = null, // TODO: KindArray
val eventIds: HexArray? = null,
val authorPublicKeys: HexArray? = null, // TODO: PublicKeyArray
val kinds: KindArray? = null, // TODO: KindArray
val tagName: String? = null,
val since: Instant? = null,
val until: Instant? = null,
val limit: Long? = null,
val limit: Int? = null,
val search: String? = null,
override val nostrEventId: HexKey? = null,
override val unsignedNostrEventId: Long? = null,
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = createdAt,
): OptionalNostrEventEntity, UnsignedNostrEventEntity, TimestampedEntity
): OptionalNostrEventEntity, UnsignedNostrEventEntity, TimestampedEntity {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as SynchronizeNostrEventRequest
if (limit != other.limit) return false
if (unsignedNostrEventId != other.unsignedNostrEventId) return false
if (id != other.id) return false
if (status != other.status) return false
if (relayURL != other.relayURL) return false
if (!eventIds.contentEquals(other.eventIds)) return false
if (!authorPublicKeys.contentEquals(other.authorPublicKeys)) return false
if (!kinds.contentEquals(other.kinds)) return false
if (tagName != other.tagName) return false
if (since != other.since) return false
if (until != other.until) return false
if (nostrEventId != other.nostrEventId) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
return true
}
override fun hashCode(): Int {
var result = limit?.hashCode() ?: 0
result = 31 * result + (unsignedNostrEventId?.hashCode() ?: 0)
result = 31 * result + id.hashCode()
result = 31 * result + status.hashCode()
result = 31 * result + relayURL.hashCode()
result = 31 * result + (eventIds?.contentHashCode() ?: 0)
result = 31 * result + (authorPublicKeys?.contentHashCode() ?: 0)
result = 31 * result + (kinds?.contentHashCode() ?: 0)
result = 31 * result + (tagName?.hashCode() ?: 0)
result = 31 * result + (since?.hashCode() ?: 0)
result = 31 * result + (until?.hashCode() ?: 0)
result = 31 * result + (nostrEventId?.hashCode() ?: 0)
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()
return result
}
}

View File

@@ -0,0 +1,4 @@
package ac.aux.compose.database.model.typealiases
typealias HexArray = Array<String>

View File

@@ -0,0 +1,3 @@
package ac.aux.compose.database.model.typealiases
typealias KindArray = Array<Int>

View File

@@ -81,18 +81,19 @@ class DatabaseNostrRepository(
// Look busy for the onboarding screen
delay(3_500)
if (unsignedNostrEvent.signedAt != null) {
// Submit a SyncNostrEventRequest
synchronizationRelayURLs.forEach { relayUrl ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
authorPublicKeys = unsignedNostrEvent.pubKey,
status = "pending",
relayURL = relayUrl
)
)
}
}
// if (unsignedNostrEvent.signedAt != null) {
// // Submit a SyncNostrEventRequest
// synchronizationRelayURLs.forEach { relayUrl ->
// database.synchronizeNostrEventRequestDao().upsert(
// SynchronizeNostrEventRequest(
// authorPublicKeys = arrayOf(unsignedNostrEvent.pubKey),
// kinds = arrayOf(0),
// status = "pending",
// relayURL = relayUrl
// )
// )
// }
// }
}
}
@@ -189,7 +190,8 @@ class DatabaseNostrRepository(
database.synchronizeNostrEventRequestDao().upsert(
synchronizeNostrEventRequest.copy(
nostrEventId = nostrEvent.id
nostrEventId = nostrEvent.id,
status = "processed"
)
)
}

View File

@@ -28,11 +28,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun UnqueuedProfileSynchronizationScreen(
unsignedNostrEventId: Long,
profilePublicKey: String,
nostrRepository: NostrRepository
) {
val unqueuedProfileSynchronizationViewModel: UnqueuedProfileSynchronizationViewModel = viewModel(
factory = UnqueuedProfileSynchronizationViewModel.factory(
unsignedNostrEventId = unsignedNostrEventId,
profilePublicKey = profilePublicKey,
nostrRepository = nostrRepository
)
@@ -90,6 +92,7 @@ private fun UnqueuedProfileSynchronizationScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
UnqueuedProfileSynchronizationScreen(
unsignedNostrEventId = 2,
profilePublicKey = "npub is for living.",
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
)

View File

@@ -132,7 +132,8 @@ fun AuxNavHost(
is NavigationUIState.UnqueuedProfileSynchronization -> {
navController.navigate(
route = UnqueuedProfileSynchronizationRoute(
state.unsignedNostrEvent.pubKey
unsignedNostrEventId = state.unsignedNostrEvent.id,
publicKey = state.unsignedNostrEvent.pubKey
)
) {
popUpTo(0)
@@ -255,6 +256,7 @@ fun AuxNavHost(
composable<UnqueuedProfileSynchronizationRoute> { backStackEntry ->
val route = backStackEntry.toRoute<UnqueuedProfileSynchronizationRoute>()
UnqueuedProfileSynchronizationScreen(
unsignedNostrEventId = route.unsignedNostrEventId,
profilePublicKey = route.publicKey,
nostrRepository = nostrRepository
)

View File

@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class UnqueuedProfileSynchronizationRoute(
val publicKey: String,
val unsignedNostrEventId: Long,
): Route() {
}

View File

@@ -12,7 +12,10 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.utils.text
import io.ktor.client.HttpClient
@@ -34,9 +37,7 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlin.collections.set
import kotlin.time.Instant
@@ -91,7 +92,7 @@ class NavigationViewModel(
observeUnsignedNostrEvents()
observePendingBroadcastNostrEventRequests()
observeProfile()
observeSyncNostrEventRequests()
observePendingSyncNostrEventRequests()
}
@@ -133,118 +134,123 @@ class NavigationViewModel(
}
}
private fun observeSyncNostrEventRequests() {
logger.i { "observeSyncNostrEventRequests" }
private fun observePendingSyncNostrEventRequests() {
logger.i { "observePendingSyncNostrEventRequests" }
scope.launch(Dispatchers.IO) {
nostrRepository.observePendingSynchronizeNostrEventRequests().collect { synchronizeNostrEventRequestOrNull ->
synchronizeNostrEventRequestOrNull?.let { synchronizeNostrEventRequest ->
val filterContent = mutableMapOf<String, JsonElement>()
synchronizeNostrEventRequest.eventIds?.let { eventIds ->
filterContent.put("ids", JsonPrimitive(eventIds))
}
synchronizeNostrEventRequest.authorPublicKeys?.let { authorPublicKeys ->
filterContent.put("authors", JsonPrimitive(authorPublicKeys))
}
synchronizeNostrEventRequest.kinds?.let { kinds ->
filterContent.put("kinds", JsonPrimitive(kinds))
}
synchronizeNostrEventRequest.since?.let { since ->
filterContent.put("since", JsonPrimitive(since.toEpochMilliseconds()))
}
synchronizeNostrEventRequest.until?.let { until ->
filterContent.put("until", JsonPrimitive(until.toEpochMilliseconds()))
}
synchronizeNostrEventRequest.limit?.let { limit ->
filterContent.put("limit", JsonPrimitive(limit))
}
val filter = JsonObject(
filterContent
)
logger.d("Syncing: $filter")
scope.launch {
val webSocketSession = httpClient.webSocketSession(
urlString = synchronizeNostrEventRequest.relayURL
try {
val reqCommand = ReqCmd(
subId = synchronizeNostrEventRequest.id,
filters = listOf(
Filter(
ids = synchronizeNostrEventRequest.eventIds?.map { eventId -> eventId },
authors = synchronizeNostrEventRequest.authorPublicKeys?.map { authorPublicKey -> authorPublicKey },
kinds = synchronizeNostrEventRequest.kinds?.map { kind -> kind },
since = synchronizeNostrEventRequest.since?.toEpochMilliseconds(),
until = synchronizeNostrEventRequest.until?.toEpochMilliseconds(),
limit = synchronizeNostrEventRequest.limit,
search = synchronizeNostrEventRequest.search
)
)
)
syncingJobs[synchronizeNostrEventRequest.relayURL] = scope.launch {
webSocketSession.send(
frame = Frame.Text("[\"REQ\",${synchronizeNostrEventRequest.id},${filter}]")
scope.launch {
val webSocketSession = httpClient.webSocketSession(
urlString = synchronizeNostrEventRequest.relayURL
)
nostrRepository.synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest)
syncingJobs[synchronizeNostrEventRequest.relayURL] = scope.launch {
val filterRequest = OptimizedJsonMapper.toJson(reqCommand)
logger.d("Syncing: $filterRequest")
webSocketSession.send(
frame = Frame.Text(filterRequest)
)
webSocketSession.incoming.consumeAsFlow().collect { frame ->
when(frame) {
is Frame.Text -> {
frame.readText().let { text ->
try {
logger.d("WebSocket Read Text: $text")
val result = Json.decodeFromString<JsonArray>(text)
nostrRepository.synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest)
if (result.firstOrNull()?.text == "EVENT") {
result.getOrNull(1)?.text?.let { subscriptionId ->
if (subscriptionId == synchronizeNostrEventRequest.id) {
webSocketSession.incoming.consumeAsFlow().collect { frame ->
when(frame) {
is Frame.Text -> {
frame.readText().let { text ->
try {
logger.d("WebSocket Read Text: $text")
val result = Json.decodeFromString<JsonArray>(text)
result.getOrNull(2)?.text?.let { jsonText ->
val event = Event.fromJson(
jsonText
)
if (result.firstOrNull()?.text == "EVENT") {
result.getOrNull(1)?.text?.let { subscriptionId ->
if (subscriptionId == synchronizeNostrEventRequest.id) {
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
)
result.getOrNull(2)?.jsonObject?.toString()?.let { jsonText ->
logger.i("Processing: $jsonText")
val event = Event.fromJson(
jsonText
)
nostrRepository.saveNostrEvent(
nostrEvent = nostrEvent,
synchronizeNostrEventRequest,
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
val nostrEvent = NostrEvent(
id = event.id,
pubKey = event.pubKey,
kind = event.kind,
content = event.content,
tags = event.tags,
createdAt = Instant.fromEpochMilliseconds(event.createdAt),
unsignedNostrEventId = synchronizeNostrEventRequest.unsignedNostrEventId,
sig = event.sig
)
nostrRepository.saveNostrEvent(
nostrEvent = nostrEvent,
synchronizeNostrEventRequest,
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
}
}
} else if(result.firstOrNull()?.text == "EOSE") {
result.getOrNull(1)?.text?.let { subscriptionId ->
if (subscriptionId == synchronizeNostrEventRequest.id) {
// Close the job
logger.d("We need to close the job")
}
}
} else {
logger.d("Unsupported: $text")
}
} else {
logger.d("Unsupported: $text")
} catch (e: Throwable) {
logger.e("Error processing receipt: $text", e)
}
} catch (e: Throwable) {
logger.e("Error processing receipt: $text", e)
}
// Syncing doesn't necessary have to close after result..
webSocketSession.close()
syncingJobs.remove(synchronizeNostrEventRequest.relayURL)?.cancel()
}
is Frame.Close -> {
logger.d("Close $frame")
}
is Frame.Ping -> {
logger.d("Ping: $frame")
}
is Frame.Pong -> {
logger.d("Pong: $frame")
}
is Frame.Binary -> {
logger.d("Binary Message: $frame")
}
else -> {
logger.d("Unsupported frame: $frame")
}
// Syncing doesn't necessary have to close after result..
webSocketSession.close()
syncingJobs.remove(synchronizeNostrEventRequest.relayURL)?.cancel()
}
is Frame.Close -> {
logger.d("Close $frame")
}
is Frame.Ping -> {
logger.d("Ping: $frame")
}
is Frame.Pong -> {
logger.d("Pong: $frame")
}
is Frame.Binary -> {
logger.d("Binary Message: $frame")
}
else -> {
logger.d("Unsupported frame: $frame")
}
}
logger.i("Websocket exit")
}
logger.i("Websocket exit")
syncingJobs[synchronizeNostrEventRequest.relayURL]?.join()
}
syncingJobs[synchronizeNostrEventRequest.relayURL]?.join()
} catch (e: Throwable) {
logger.e("Relay Error", e)
}
}
}

View File

@@ -19,9 +19,11 @@ 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.delay
import kotlinx.coroutines.launch
class UnqueuedProfileSynchronizationViewModel(
val unsignedNostrEventId: Long,
val profilePublicKey: String,
val nostrRepository: NostrRepository
): ViewModel() {
@@ -29,11 +31,13 @@ class UnqueuedProfileSynchronizationViewModel(
private const val TAG = "UnqueuedProfileSynchronizationViewModel"
fun factory(
unsignedNostrEventId: Long,
profilePublicKey: String,
nostrRepository: NostrRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
UnqueuedProfileSynchronizationViewModel(
unsignedNostrEventId = unsignedNostrEventId,
profilePublicKey = profilePublicKey,
nostrRepository = nostrRepository
)
@@ -49,11 +53,14 @@ class UnqueuedProfileSynchronizationViewModel(
logger.d { "queueSynchronization" }
viewModelScope.launch(Dispatchers.IO) {
delay(2_000)
nostrRepository.queueSynchronizeNostrEvent(
Relays.eventPublishRelaySet.map { normalizedRelayUrl ->
SynchronizeNostrEventRequest(
authorPublicKeys = profilePublicKey,
kinds = "0",
authorPublicKeys = arrayOf(profilePublicKey),
unsignedNostrEventId = unsignedNostrEventId,
kinds = arrayOf(0),
relayURL = normalizedRelayUrl.url
)
}