Create a profile

This commit is contained in:
Kgothatso Ngako
2026-03-26 03:46:44 +02:00
parent 3d045fbf8d
commit f73a46ae5c
12 changed files with 343 additions and 136 deletions

View File

@@ -1,11 +1,11 @@
package ac.aux.compose.database.repository
import ac.aux.compose.database.AuxDatabase
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
import ac.aux.compose.repository.NostrRepository
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import kotlinx.coroutines.delay
@@ -15,6 +15,12 @@ import kotlin.time.Instant
class DatabaseNostrRepository(
private val database: AuxDatabase
): NostrRepository {
companion object {
const val TAG = "DatabaseNostrRepository"
}
val logger = Logger.withTag(TAG)
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
return database.profileDao().observeProfile(publicKey)
}
@@ -24,8 +30,6 @@ class DatabaseNostrRepository(
name: String,
biography: String,
onError: () -> Unit,
onUnsignProfile: (UnsignedNostrEvent) -> Unit,
onUnbroadcastedProfile: (NostrEvent) -> Unit,
onProfileReady: (Profile) -> Unit
) {
val profileEventTemplate = MetadataEvent.createNew(
@@ -43,34 +47,7 @@ class DatabaseNostrRepository(
)
)
if (unsignedNostrEvent != null) {
onUnsignProfile.invoke(unsignedNostrEvent)
delay(7_500) // Seeming busy is very important...
// TODO: Call and collect kind 0 nostrEvent with pubKey
onUnbroadcastedProfile.invoke(
NostrEvent(
id = "",
pubKey = "",
kind = 0,
content = "Something here",
tags = emptyArray(),
sig = ""
)
)
delay(4_500) // Seeming busy is very important...
onProfileReady.invoke(
Profile(
publicKey = "",
nostrEventId = "",
displayName = name,
about = biography,
)
)
} else {
if (unsignedNostrEvent == null) {
onError.invoke()
}
}
@@ -81,6 +58,7 @@ class DatabaseNostrRepository(
id = database.unsignedNostrEventDao().upsert(unsignedNostrEvent)
)
} catch (e: Throwable) {
logger.e("Failed to saveUnsignedNostrEvent", e)
null
}

View File

@@ -1,6 +1,5 @@
package ac.aux.compose.repository
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
@@ -15,8 +14,8 @@ interface NostrRepository {
name: String,
biography: String,
onError: () -> Unit,
onUnsignProfile: (UnsignedNostrEvent) -> Unit,
onUnbroadcastedProfile: (NostrEvent) -> Unit,
// onUnsignProfile: (UnsignedNostrEvent) -> Unit,
// onUnbroadcastedProfile: (NostrEvent) -> Unit,
onProfileReady: (Profile) -> Unit
)
@@ -35,8 +34,6 @@ interface NostrRepository {
name: String,
biography: String,
onError: () -> Unit,
onUnsignProfile: (UnsignedNostrEvent) -> Unit,
onUnbroadcastedProfile: (NostrEvent) -> Unit,
onProfileReady: (Profile) -> Unit
) {
}

View File

@@ -1,10 +1,9 @@
package ac.aux.compose.ui.composable
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.repository.NostrRepository
import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator
import ac.aux.compose.ui.composable.navigation.routes.UnannouncedProfileRoute
import ac.aux.compose.ui.composable.navigation.routes.UnsignedProfileRoute
import ac.aux.compose.ui.theme.AuxTheme
import ac.aux.compose.ui.theme.BluePill
import ac.aux.compose.ui.theme.RedPill
@@ -297,54 +296,6 @@ fun CreateProfileScreen(
modifier = Modifier.weight(1f)
)
}
is CreateProfileUIState.UnsignedProfile -> {
Spacer(
modifier = Modifier.weight(1f)
)
Text(
text = "Your profile is almost ready... just getting it's first cryptographic signature together.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = "As long as you control your keys there can be no dispute about who ${createAccountUIState.name} actually is.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
LoadingDataIndicator(
fillScreen = false
)
Spacer(
modifier = Modifier.weight(2f)
)
}
is CreateProfileUIState.UnbroadcastedProfile -> {
Spacer(
modifier = Modifier.weight(1f)
)
Text(
text = "Everything is cryptographical sound. Just announcing your profile to the world.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = "Aux will be broadcast to what you publish to relays so that it's distributed and decentralized.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
LoadingDataIndicator(
fillScreen = false
)
Spacer(
modifier = Modifier.weight(2f)
)
}
is CreateProfileUIState.ProfileReady -> {
Spacer(
@@ -400,8 +351,6 @@ fun CreateProfileScreen(
)
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
package ac.aux.compose.ui.composable
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.repository.NostrRepository
import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator
import ac.aux.compose.ui.theme.AuxTheme
import ac.aux.compose.ui.theme.BluePill
import ac.aux.compose.ui.theme.RedPill
import ac.aux.compose.ui.view.model.CreateProfileViewModel
import ac.aux.compose.ui.view.state.CreateProfileUIState
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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Badge
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
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 UnannouncedProfileScreen(
nostrEvent: NostrEvent,
) {
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 = "Everything is cryptographical sound. Just announcing your profile to the world.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = "Aux will be broadcast to what you publish to relays so that it's distributed and decentralized.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.weight(1f)
)
LoadingDataIndicator(
fillScreen = false
)
Spacer(
modifier = Modifier.weight(8f)
)
// TODO: Give user ability to start over...
}
}
}
}
@Preview
@Composable
private fun UnannouncedProfileScreenPreview() {
AuxTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
UnannouncedProfileScreen(
nostrEvent = NostrEvent(
id = "",
pubKey = "",
kind = 0,
content = "Something here",
tags = emptyArray(),
sig = ""
),
)
}
}
}

View File

@@ -0,0 +1,117 @@
package ac.aux.compose.ui.composable
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.repository.NostrRepository
import ac.aux.compose.ui.composable.navigation.routes.UnannouncedProfileRoute
import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator
import ac.aux.compose.ui.theme.AuxTheme
import ac.aux.compose.ui.theme.BluePill
import ac.aux.compose.ui.theme.RedPill
import ac.aux.compose.ui.view.model.CreateProfileViewModel
import ac.aux.compose.ui.view.state.CreateProfileUIState
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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Badge
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
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
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun UnsignedProfileScreen(
unsignedNostrEvent: UnsignedNostrEvent
) {
val metadataEvent = MetadataEvent(
id = unsignedNostrEvent.id.toString(),
pubKey = unsignedNostrEvent.pubKey,
createdAt = unsignedNostrEvent.createdAt.toEpochMilliseconds(),
tags = unsignedNostrEvent.tags,
content = unsignedNostrEvent.content,
sig = ""
)
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 = "Your profile is almost ready... just getting it's first cryptographic signature together.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = "As long as you control your keys there can be no dispute about who ${metadataEvent.contactMetaData()?.displayName ?: "YOU"} actually is.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
LoadingDataIndicator(
fillScreen = false
)
Spacer(
modifier = Modifier.weight(2f)
)
}
}
}
}
@Preview
@Composable
private fun UnsignedProfileScreenPreview() {
AuxTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
UnsignedProfileScreen(
unsignedNostrEvent = UnsignedNostrEvent(
pubKey = "",
kind = 0,
content = "Something here",
tags = emptyArray(),
)
)
}
}
}

View File

@@ -1,7 +1,6 @@
package ac.aux.compose.ui.composable.navigation
import ac.aux.compose.AuxGlobal
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.repository.DatabaseNostrRepository
import ac.aux.compose.managers.DatabaseManager
import ac.aux.compose.ui.composable.CreateProfileScreen
@@ -10,6 +9,8 @@ import ac.aux.compose.ui.composable.ImplementationPendingScreen
import ac.aux.compose.ui.composable.LandingScreen
import ac.aux.compose.ui.composable.LoadingScreen
import ac.aux.compose.ui.composable.SocialPreconditionScreen
import ac.aux.compose.ui.composable.UnannouncedProfileScreen
import ac.aux.compose.ui.composable.UnsignedProfileScreen
import ac.aux.compose.ui.composable.navigation.routes.BlankRoute
import ac.aux.compose.ui.composable.navigation.routes.CreateProfileRoute
import ac.aux.compose.ui.composable.navigation.routes.FeedRoute
@@ -21,11 +22,11 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import ac.aux.compose.ui.composable.navigation.routes.Route
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.UnsignedProfileRoute
import ac.aux.compose.ui.composable.widgets.feed.DUMMY_EVENTS
import ac.aux.compose.ui.view.model.NavigationViewModel
import ac.aux.compose.ui.view.state.CreateProfileUIState
import ac.aux.compose.ui.view.state.FeedListUIState
import ac.aux.compose.ui.view.state.NavigationUIState
import androidx.compose.foundation.layout.fillMaxSize
@@ -82,10 +83,17 @@ fun AuxNavHost(
popUpTo(0)
}
}
is NavigationUIState.UnbroadcastedProfile -> {
is NavigationUIState.UnsignedProfile -> {
navController.navigate(
route = CreateProfileRoute(
nostrEventId = state.nostrEvent.id
route = UnsignedProfileRoute
) {
popUpTo(0)
}
}
is NavigationUIState.UnannouncedProfile -> {
navController.navigate(
route = UnannouncedProfileRoute(
state.nostrEvent.id
)
) {
popUpTo(0)
@@ -93,7 +101,7 @@ fun AuxNavHost(
}
is NavigationUIState.ProfileLoaded -> {
navController.navigate(
route = FeedRoute
route = SocialPreconditionRoute
) {
popUpTo(0)
}
@@ -139,11 +147,6 @@ fun AuxNavHost(
val route = backStackEntry.toRoute<CreateProfileRoute>()
CreateProfileScreen(
initialCreateProfileUIState = route.toPlaceholderNostrEvent()?.let { placeholderNostrEvent ->
CreateProfileUIState.UnbroadcastedProfile(
placeholderNostrEvent
)
} ?: CreateProfileUIState.Declaration,
onNavigateToSocialPreconditionRoute = {
navController.navigate(
route = SocialPreconditionRoute
@@ -159,6 +162,20 @@ fun AuxNavHost(
nostrRepository = nostrRepository
)
}
composable<UnsignedProfileRoute> { backStackEntry ->
val route = backStackEntry.toRoute<UnsignedProfileRoute>()
UnsignedProfileScreen(
unsignedNostrEvent = route.toPlaceholderUnsignedNostrEvent()
)
}
composable<UnannouncedProfileRoute> { backStackEntry ->
val route = backStackEntry.toRoute<UnannouncedProfileRoute>()
UnannouncedProfileScreen(
nostrEvent = route.toPlaceholderNostrEvent()
)
}
composable<SocialPreconditionRoute> {
SocialPreconditionScreen(
onNavigateToSkipForNow = {

View File

@@ -0,0 +1,22 @@
package ac.aux.compose.ui.composable.navigation.routes
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.Profile
import kotlinx.serialization.Serializable
import kotlin.time.Clock
@Serializable
data class UnannouncedProfileRoute(
val nostrEventId: String,
): Route() {
fun toPlaceholderNostrEvent(): NostrEvent = NostrEvent(
id = nostrEventId,
pubKey = "",
createdAt = Clock.System.now(),
tags = emptyArray(),
kind = 0,
sig = "",
content = ""
)
}

View File

@@ -0,0 +1,20 @@
package ac.aux.compose.ui.composable.navigation.routes
import ac.aux.compose.database.model.UnsignedNostrEvent
import kotlinx.serialization.Serializable
import kotlin.time.Clock
@Serializable
data class UnsignedProfileRoute(
val unsignedNostrEventId: Long,
): Route() {
fun toPlaceholderUnsignedNostrEvent(): UnsignedNostrEvent = UnsignedNostrEvent(
id = unsignedNostrEventId,
pubKey = "",
createdAt = Clock.System.now(),
tags = emptyArray(),
kind = 0,
content = ""
)
}

View File

@@ -64,7 +64,10 @@ class CreateProfileViewModel(
return true
}
public fun createAccount() {
public fun createAccount(
// onNavigateToUnsignedProfile: (UnsignedProfileRoute) -> Unit,
// onNavigateToUnannouncedProfile: (UnannouncedProfileRoute) -> Unit
) {
logger.d { "createAccount" }
isActionPending.value = true
@@ -77,18 +80,20 @@ class CreateProfileViewModel(
onError = {
createProfileUIState.value = CreateProfileUIState.Error
},
onUnsignProfile = { unsignedNostrEvent ->
createProfileUIState.value = CreateProfileUIState.UnsignedProfile(
unsignedNostrEvent = unsignedNostrEvent,
name = createProfileFormState.nameField.text.value,
bio = createProfileFormState.biographyField.text.value
)
},
onUnbroadcastedProfile = { unbroadcastedNostrEvent ->
createProfileUIState.value = CreateProfileUIState.UnbroadcastedProfile(
nostrEvent = unbroadcastedNostrEvent,
)
}
// onUnsignProfile = { unsignedNostrEvent ->
// onNavigateToUnsignedProfile.invoke(
// UnsignedProfileRoute(
// unsignedNostrEvent.id
// )
// )
// },
// onUnbroadcastedProfile = { unannouncedNostrEvent ->
// onNavigateToUnannouncedProfile.invoke(
// UnannouncedProfileRoute(
// nostrEventId = unannouncedNostrEvent.id
// )
// )
// }
) { profile ->
createProfileUIState.value = CreateProfileUIState.ProfileReady(
profile = profile

View File

@@ -10,7 +10,6 @@ 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.nip01Core.crypto.KeyPair
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.random.Random
class NavigationViewModel(
initialNavigationUIState: NavigationUIState,
@@ -72,20 +70,20 @@ class NavigationViewModel(
profile = localProfile.profile
)
} else if (localProfile.nostrEvent != null) {
NavigationUIState.UnbroadcastedProfile(
NavigationUIState.UnannouncedProfile(
nostrEvent = localProfile.nostrEvent
)
// TODO: Support unsignedNostrEvent with pubKey so we can have multi account support...
// } else if (localProfile.nostrEvent != null) {
// NavigationUIState.UnbroadcastedProfile(
// nostrEvent = localProfile.nostrEvent
// )
} else {
NavigationUIState.Error
NavigationUIState.UnsignedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
}
}
}.catch {
NavigationUIState.Error
}.catch { e ->
logger.e("Error observing profile:", e)
_navigationUIState.getAndUpdate {
NavigationUIState.Error
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NavigationUIState.Landing)
}
}

View File

@@ -16,16 +16,6 @@ abstract class CreateProfileUIState {
val bio: String
) : CreateProfileUIState()
data class UnsignedProfile(
val unsignedNostrEvent: UnsignedNostrEvent,
val name: String,
val bio: String,
) : CreateProfileUIState()
data class UnbroadcastedProfile(
val nostrEvent: NostrEvent,
) : CreateProfileUIState()
data class ProfileReady(
val profile: Profile
) : CreateProfileUIState()

View File

@@ -15,7 +15,7 @@ abstract class NavigationUIState {
val unsignedNostrEvent: UnsignedNostrEvent,
) : NavigationUIState()
data class UnbroadcastedProfile(
data class UnannouncedProfile(
val nostrEvent: NostrEvent,
) : NavigationUIState()