Improve create profile flow

This commit is contained in:
Kgothatso Ngako committed 2026-05-19 00:33:33 +02:00
1 parent 9264e95af0
commit e4e7d6570b
8 files changed
+168 -190

No files matched your search

@@ -58,28 +58,6 @@ abstract class NostrDao(
signedAt = Clock.System.now() signedAt = Clock.System.now()
) )
) )
// Find or create profile with the pubKey... if not found TODO: 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 = DraftWrapEvent.KIND,
// pubKey = placeHolderProfile.publicKey,
// content = placeHolderProfile.publicKey,
// createdAt = GENESIS_AT,
// sig = placeHolderProfile.publicKey,
// tags = emptyArray(),
// )
// database.nostrEventDao().upsert(placeHolderProfileNostrEvent)
// database.profileDao().upsert(placeHolderProfile)
//
// // TODO: Sync to find and update profile
// }
database.nostrEventDao().upsert(nostrEvent) database.nostrEventDao().upsert(nostrEvent)
@@ -3,6 +3,7 @@ package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount
import androidx.room3.Dao import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.Query import androidx.room3.Query
import androidx.room3.Transaction import androidx.room3.Transaction
import androidx.room3.Upsert import androidx.room3.Upsert
@@ -22,10 +23,13 @@ interface UnsignedNostrEventDao {
@Upsert @Upsert
suspend fun upsert(unsignedNostrEvent: UnsignedNostrEvent): Long suspend fun upsert(unsignedNostrEvent: UnsignedNostrEvent): Long
@Insert
suspend fun insert(unsignedNostrEvents: List<UnsignedNostrEvent>)
@Transaction @Transaction
@Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0 AND pubKey = :publicKey") @Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0 AND pubKey = :publicKey")
fun observeProfile(publicKey: String): Flow<LocalAccount?> fun observeProfile(publicKey: String): Flow<LocalAccount?>
@Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL") // TODO: where nostrEvent.unsignedNostrEventId is 0 @Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL")
fun observeUnsignedNostrEvents(publicKey: String): Flow<UnsignedNostrEvent?> fun observeUnsignedNostrEvents(publicKey: String): Flow<UnsignedNostrEvent?>
} }
@@ -63,7 +63,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlin.collections.emptyMap import kotlin.collections.emptyMap
import kotlin.collections.plus import kotlin.collections.plus
import kotlin.math.sin
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -116,132 +115,129 @@ class DatabaseNostrRepository(
override suspend fun createNewProfile( override suspend fun createNewProfile(
publicKey: HexKey, publicKey: HexKey,
name: String?, name: String?,
biography: String?, biography: String?
onError: () -> Unit,
onProfileReady: (Profile) -> Unit,
synchronizationRelayURLs: List<String>
) { ) {
val profileEventTemplate = MetadataEvent.createNew( val profileEventTemplate = MetadataEvent.createNew(
name = name, name = name,
about = biography, about = biography,
) )
val signedAt = if (name == null && biography == null) { val unsignedNostrEvents = mutableListOf(
// We are most likely try to sign in to an already existing block...
GENESIS_AT
} else {
null
}
val unsignedNostrEvent = saveUnsignedNostrEvent(
UnsignedNostrEvent( UnsignedNostrEvent(
pubKey = publicKey, pubKey = publicKey,
kind = profileEventTemplate.kind, kind = profileEventTemplate.kind,
createdAt = Instant.fromEpochSeconds(profileEventTemplate.createdAt), createdAt = Instant.fromEpochSeconds(profileEventTemplate.createdAt),
tags = profileEventTemplate.tags, tags = profileEventTemplate.tags,
content = profileEventTemplate.content, content = profileEventTemplate.content,
signedAt = signedAt // This will keep us from trying to sign this event... but we should automatically sync the npub profile signedAt = null
) )
) )
if (unsignedNostrEvent == null) { val followUsers = listOf(ContactTag(publicKey, null, null))
onError.invoke() val contactListTagList = listOf(AltTag.assemble(ContactListEvent.ALT)) +
} else if (unsignedNostrEvent.kind == 0) { followUsers.map { it.toTagArray() }
// Look busy for the onboarding screen
delay(3_500) unsignedNostrEvents.add(
UnsignedNostrEvent(
pubKey = publicKey,
kind = ContactListEvent.KIND,
tags = contactListTagList.toTypedArray(),
content = RelaySet.assemble(
emptyMap()
)
)
)
unsignedNostrEvents.add(
UnsignedNostrEvent(
pubKey = publicKey,
kind = ChatMessageRelayListEvent.KIND,
tags = ChatMessageRelayListEvent.createTagArray(
Relays.DefaultDMRelayList
),
content = ""
)
)
unsignedNostrEvents.add(
UnsignedNostrEvent(
pubKey = publicKey,
kind = AdvertisedRelayListEvent.KIND,
tags = Relays.DefaultNIP65List.map{ it.toTagArray() }.toTypedArray(),
content = ""
)
)
if (unsignedNostrEvent.signedAt == null) {
// Save ContactListNostrEvent
val followUsers = listOf(ContactTag(publicKey, null, null))
val contactListTagList = listOf(AltTag.assemble(ContactListEvent.ALT)) +
followUsers.map { it.toTagArray() }
saveUnsignedNostrEvent( unsignedNostrEvents.add(
UnsignedNostrEvent( UnsignedNostrEvent(
pubKey = publicKey, pubKey = publicKey,
kind = ContactListEvent.KIND, kind = SearchRelayListEvent.KIND,
tags = contactListTagList.toTypedArray(), tags = Relays.DefaultSearchRelayList.map{ RelayTag.assemble(it) }
content = RelaySet.assemble( .plus(
emptyMap() SearchRelayListEvent.ALT_TAG
) ).toTypedArray(),
) privateTags = emptyArray(),
) content = ""
)
)
// Save AdvertisedRelayListEvent unsignedNostrEvents.add(
saveUnsignedNostrEvent( UnsignedNostrEvent(
UnsignedNostrEvent( pubKey = publicKey,
pubKey = publicKey, kind = IndexerRelayListEvent.KIND,
kind = AdvertisedRelayListEvent.KIND, tags = arrayOf(AltTag.assemble(IndexerRelayListEvent.ALT)),
tags = Relays.DefaultNIP65List.map{ it.toTagArray() }.toTypedArray(), privateTags = Relays.DefaultIndexerRelayList.map { RelayTag.assemble(it) }.toTypedArray() ,
content = "" content = ""
)
)
unsignedNostrEvents.add(
UnsignedNostrEvent(
pubKey = publicKey,
kind = ChannelListEvent.KIND,
tags = arrayOf(
AltTag.assemble(
ChannelListEvent.ALT
) )
) ),
// Save ChatMessageRelayListEvent privateTags = Relays.DefaultChannels.map { it.toTagArray() }.toTypedArray(),
saveUnsignedNostrEvent( content = ""
UnsignedNostrEvent( )
pubKey = publicKey, )
kind = ChatMessageRelayListEvent.KIND,
tags = Relays.DefaultDMRelayList.map{ RelayTag.assemble(it) } unsignedNostrEvents.add(
.plusElement( UnsignedNostrEvent(
AltTag.assemble("Relay list to receive private messages") pubKey = publicKey,
).toTypedArray(), kind = RelayFeedsListEvent.KIND,
content = "" tags = arrayOf(
) AltTag.assemble(RelayFeedsListEvent.ALT)
) ),
// Save SearchRelayListEvent privateTags = Relays.DefaultGlobalRelays.map { RelayTag.assemble(it)}.toTypedArray(),
saveUnsignedNostrEvent( content = ""
UnsignedNostrEvent( )
pubKey = publicKey, )
kind = SearchRelayListEvent.KIND,
tags = Relays.DefaultSearchRelayList.map{ RelayTag.assemble(it) } saveUnsignedNostrEvents(unsignedNostrEvents)
.plus(
SearchRelayListEvent.ALT_TAG
).toTypedArray(),
privateTags = emptyArray(),
content = ""
)
)
// Save IndexerRelayListEvent
saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = publicKey,
kind = IndexerRelayListEvent.KIND,
tags = arrayOf(AltTag.assemble(IndexerRelayListEvent.ALT)),
privateTags = Relays.DefaultIndexerRelayList.map { RelayTag.assemble(it) }.toTypedArray() ,
content = ""
)
)
// Save ChannelListEvent
saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = publicKey,
kind = ChannelListEvent.KIND,
tags = arrayOf(
AltTag.assemble(
ChannelListEvent.ALT
)
),
privateTags = Relays.DefaultChannels.map { it.toTagArray() }.toTypedArray(),
content = ""
)
)
// Save RelayFeedsListEvent
saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = publicKey,
kind = RelayFeedsListEvent.KIND,
tags = arrayOf(
AltTag.assemble(RelayFeedsListEvent.ALT)
),
privateTags = Relays.DefaultGlobalRelays.map { RelayTag.assemble(it)}.toTypedArray(),
content = ""
)
)
}
}
} }
override suspend fun signInToProfile(
publicKey: HexKey
) {
val profileEventTemplate = MetadataEvent.createNew()
saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = publicKey,
kind = profileEventTemplate.kind,
createdAt = Instant.fromEpochSeconds(profileEventTemplate.createdAt),
tags = profileEventTemplate.tags,
content = profileEventTemplate.content,
signedAt = GENESIS_AT // This will keep us from trying to sign this event... but we should automatically sync the npub profile
)
)
}
override suspend fun createNewTextNote( override suspend fun createNewTextNote(
publicKey: HexKey, publicKey: HexKey,
mentionedPublicKeys: List<HexKey>, mentionedPublicKeys: List<HexKey>,
@@ -362,8 +358,12 @@ class DatabaseNostrRepository(
} }
override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? = try { override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? = try {
logger.d("Save unsigned: ${unsignedNostrEvent.kind}")
// TODO: We can actually check the unsignedNostrEvent.pubKey for support... if it's writeable // TODO: We can actually check the unsignedNostrEvent.pubKey for support... if it's writeable
delay(2_100) // Just to make it look like we are doing serious work... if (unsignedNostrEvent.kind == MetadataEvent.KIND){
delay(2_100) // Just to make it look like we are doing serious work...
}
unsignedNostrEvent.copy( unsignedNostrEvent.copy(
id = database.unsignedNostrEventDao().upsert(unsignedNostrEvent) id = database.unsignedNostrEventDao().upsert(unsignedNostrEvent)
) )
@@ -372,6 +372,15 @@ class DatabaseNostrRepository(
null null
} }
override suspend fun saveUnsignedNostrEvents(unsignedNostrEvents: List<UnsignedNostrEvent>) {
logger.d("saveUnsignedNostrEvents: $unsignedNostrEvents")
delay(2_100)
database.unsignedNostrEventDao().insert(
unsignedNostrEvents
)
}
override suspend fun wipeDatabase() { override suspend fun wipeDatabase() {
database.wipeData() database.wipeData()
} }
@@ -51,7 +51,7 @@ object Relays {
val DefaultGlobalRelays = listOf(wine, news) val DefaultGlobalRelays = listOf(wine, news)
val DefaultDMRelayList = listOf(auth, oxchat, nos) val DefaultDMRelayList = listOf(primal, oxtr, nos)
val DefaultSearchRelayList = setOf(wine, where, nostoday, antiprimal, ditto) val DefaultSearchRelayList = setOf(wine, where, nostoday, antiprimal, ditto)
@@ -40,12 +40,11 @@ interface NostrRepository {
suspend fun createNewProfile( suspend fun createNewProfile(
publicKey: HexKey, publicKey: HexKey,
name: String?, name: String?,
biography: String?, biography: String?
onError: () -> Unit, )
// onUnsignProfile: (UnsignedNostrEvent) -> Unit,
// onUnbroadcastedProfile: (NostrEvent) -> Unit, suspend fun signInToProfile(
onProfileReady: (Profile) -> Unit, publicKey: HexKey
synchronizationRelayURLs: List<String>
) )
suspend fun createNewTextNote( suspend fun createNewTextNote(
@@ -60,6 +59,8 @@ interface NostrRepository {
suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent?
suspend fun saveUnsignedNostrEvents(unsignedNostrEvents: List<UnsignedNostrEvent>)
suspend fun wipeDatabase() suspend fun wipeDatabase()
suspend fun publishNostrEvent( suspend fun publishNostrEvent(
@@ -169,13 +170,14 @@ interface NostrRepository {
override suspend fun createNewProfile( override suspend fun createNewProfile(
publicKey: HexKey, publicKey: HexKey,
name: String?, name: String?,
biography: String?, biography: String?
onError: () -> Unit,
onProfileReady: (Profile) -> Unit,
synchronizationRelayURLs: List<String>
) { ) {
} }
override suspend fun signInToProfile(publicKey: HexKey) {
TODO("Not yet implemented")
}
override suspend fun createNewTextNote( override suspend fun createNewTextNote(
publicKey: HexKey, publicKey: HexKey,
mentionedPublicKeys: List<HexKey>, mentionedPublicKeys: List<HexKey>,
@@ -192,6 +194,10 @@ interface NostrRepository {
return null return null
} }
override suspend fun saveUnsignedNostrEvents(unsignedNostrEvents: List<UnsignedNostrEvent>) {
TODO("Not yet implemented")
}
override suspend fun wipeDatabase() { override suspend fun wipeDatabase() {
} }
@@ -1,7 +1,6 @@
package ac.cord.auxiliary.compose.ui.view.model package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.CreateProfileUIState import ac.cord.auxiliary.compose.ui.view.state.CreateProfileUIState
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -78,16 +77,7 @@ class CreateProfileViewModel(
nostrRepository.createNewProfile( nostrRepository.createNewProfile(
publicKey, publicKey,
name = createProfileFormState.nameField.textFieldState.text.toString(), name = createProfileFormState.nameField.textFieldState.text.toString(),
biography = createProfileFormState.biographyField.textFieldState.text.toString(), biography = createProfileFormState.biographyField.textFieldState.text.toString()
onError = {
createProfileUIState.value = CreateProfileUIState.Error
},
{ profile ->
createProfileUIState.value = CreateProfileUIState.ProfileReady(
profile = profile
)
},
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
) )
} }
} }
@@ -104,36 +104,38 @@ class NavigationViewModel(
val tempSigner = NostrSignerSync( val tempSigner = NostrSignerSync(
SeedManager.activeKeyPair() SeedManager.activeKeyPair()
) )
logger.i { "observeUnsignedNostrEvents" }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents" }
nostrRepository.observeUnsignedNostrEvents( nostrRepository.observeUnsignedNostrEvents(
publicKey = SeedManager.activePublicKey().toHexKey() publicKey = SeedManager.activePublicKey().toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull -> ).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
unsignedNostrEventOrNull?.let { unsignedNostrEvent -> scope.launch(Dispatchers.IO) {
logger.d("Unsigned") unsignedNostrEventOrNull?.let { unsignedNostrEvent ->
val event = tempSigner.signNormal<Event>( logger.d("Unsigned: ${unsignedNostrEvent.kind}")
createdAt = unsignedNostrEvent.createdAt.epochSeconds, val event = tempSigner.signNormal<Event>(
kind = unsignedNostrEvent.kind, createdAt = unsignedNostrEvent.createdAt.epochSeconds,
tags = unsignedNostrEvent.tags, kind = unsignedNostrEvent.kind,
content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content tags = unsignedNostrEvent.tags,
) content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content
)
logger.d("Signed: ${event.toJson()}")
nostrRepository.publishNostrEvent( nostrRepository.publishNostrEvent(
unsignedNostrEvent, unsignedNostrEvent,
NostrEvent( NostrEvent(
id = event.id, id = event.id,
pubKey = event.pubKey, pubKey = event.pubKey,
kind = event.kind, kind = event.kind,
tags = event.tags, tags = event.tags,
content = event.content, content = event.content,
createdAt = Instant.fromEpochSeconds(event.createdAt), createdAt = Instant.fromEpochSeconds(event.createdAt),
sig = event.sig, sig = event.sig,
unsignedNostrEventId = unsignedNostrEvent.id unsignedNostrEventId = unsignedNostrEvent.id
), ),
relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
) )
}
} }
} }
} }
} }
@@ -1,6 +1,5 @@
package ac.cord.auxiliary.compose.ui.view.model package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.SignInToProfileUIState import ac.cord.auxiliary.compose.ui.view.state.SignInToProfileUIState
import ac.cord.auxiliary.compose.ui.view.state.form.SignInToProfileFormState import ac.cord.auxiliary.compose.ui.view.state.form.SignInToProfileFormState
@@ -72,23 +71,13 @@ class SignInToProfileViewModel(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
when (credentialEntity) { when (credentialEntity) {
is NPub -> { is NPub -> {
nostrRepository.createNewProfile( nostrRepository.signInToProfile(
publicKey = credentialEntity.hex, publicKey = credentialEntity.hex
name = null,
biography = null,
onError = onError,
onProfileReady = {},
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
) )
} }
is NSec -> { is NSec -> {
nostrRepository.createNewProfile( nostrRepository.signInToProfile(
publicKey = credentialEntity.toPubKeyHex(), publicKey = credentialEntity.toPubKeyHex()
name = null,
biography = null,
onError = onError,
onProfileReady = {},
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
) )
} }
else -> { else -> {