Add key package management
This commit is contained in:
@@ -4,9 +4,13 @@ import androidx.room3.Dao
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@Dao
|
||||
interface MarmotKeyPackageBundleDao {
|
||||
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey ORDER BY createdAt DESC")
|
||||
fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
|
||||
|
||||
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :id ORDER BY createdAt DESC")
|
||||
fun getMarmotKeyPackageBundleById(id: String): MarmotKeyPackageBundle?
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class DatabaseMarmotRepository(
|
||||
): MarmotRepository {
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
override suspend fun saveMarmotKeyPackageBundle(
|
||||
override suspend fun publishMarmotKeyPackageBundle(
|
||||
publicKey: HexKey,
|
||||
nsecPassword: String
|
||||
) {
|
||||
@@ -88,6 +88,9 @@ class DatabaseMarmotRepository(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
|
||||
return database.marmotKeyPackageBundleDao().getAllMarmotKeyPackageBundles(publicKey)
|
||||
}
|
||||
|
||||
private fun generateKeyPackage(
|
||||
identity: ByteArray,
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
package at.torch.compose.repository
|
||||
|
||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
interface MarmotRepository {
|
||||
|
||||
suspend fun saveMarmotKeyPackageBundle(
|
||||
suspend fun publishMarmotKeyPackageBundle(
|
||||
publicKey: HexKey,
|
||||
nsecPassword: String
|
||||
)
|
||||
|
||||
suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
|
||||
|
||||
companion object {
|
||||
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
|
||||
override suspend fun saveMarmotKeyPackageBundle(
|
||||
override suspend fun publishMarmotKeyPackageBundle(
|
||||
publicKey: HexKey,
|
||||
nsecPassword: String
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Bento
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.filled.SwitchVideo
|
||||
import androidx.compose.material.icons.filled.Widgets
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
@@ -42,6 +44,7 @@ import at.torch.compose.database.model.intermdiate.LocalNostrEvent
|
||||
import at.torch.compose.extensions.hexToNpubHrp
|
||||
import at.torch.compose.repository.NostrRepository
|
||||
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.KeyPackageManagementRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.Route
|
||||
import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute
|
||||
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
@@ -212,6 +215,33 @@ fun ActiveProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onNavigateToRoute.invoke(
|
||||
KeyPackageManagementRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
nostrEventId = nostrEventId
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
||||
Icon(
|
||||
Icons.Default.Bento,
|
||||
contentDescription = "Key Packages"
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.width(10.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
"Key Package Management"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
item {
|
||||
TextButton(
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package at.torch.compose.ui.composable
|
||||
|
||||
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.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Bento
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.LockReset
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.filled.PrivateConnectivity
|
||||
import androidx.compose.material.icons.filled.Publish
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.torch.compose.extensions.hexToNpubHrp
|
||||
import at.torch.compose.extensions.toFormattedTimeAndDateString
|
||||
import at.torch.compose.repository.MarmotRepository
|
||||
import at.torch.compose.repository.NostrRepository
|
||||
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.Route
|
||||
import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute
|
||||
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import at.torch.compose.ui.composable.widgets.profile.ProfileAvatar
|
||||
import at.torch.compose.ui.view.model.KeyPackageManagementViewModel
|
||||
import at.torch.compose.ui.view.state.KeyPackageManagementUIState
|
||||
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun KeyPackageManagementScreen(
|
||||
initialActiveProfileUIState: KeyPackageManagementUIState = KeyPackageManagementUIState.Loading,
|
||||
activeUserPublicKey: HexKey,
|
||||
nostrEventId: HexKey,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
marmotRepository: MarmotRepository
|
||||
) {
|
||||
|
||||
val keyPackageManagementViewModel: KeyPackageManagementViewModel = viewModel(
|
||||
factory = KeyPackageManagementViewModel.factory(
|
||||
nostrEventId = nostrEventId,
|
||||
initialActiveProfileUIState = initialActiveProfileUIState,
|
||||
marmotRepository = marmotRepository,
|
||||
activeUserPublicKey = activeUserPublicKey
|
||||
),
|
||||
)
|
||||
|
||||
when(val keyPackageManagementUIState = keyPackageManagementViewModel.keyPackageManagementUIState) {
|
||||
KeyPackageManagementUIState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Something went wrong")
|
||||
}
|
||||
}
|
||||
is KeyPackageManagementUIState.Loaded -> {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Key Package Management"
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Button(
|
||||
onClick = {
|
||||
keyPackageManagementViewModel.publishNewKeyPackage()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.LockReset,
|
||||
contentDescription = "Publish new key package"
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.width(10.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Publish New Key Package"
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
"${keyPackageManagementUIState.keyPackageBundles.size} Key Packages"
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = keyPackageManagementUIState.keyPackageBundles
|
||||
) { marmotKeyPackageBundle ->
|
||||
|
||||
Row {
|
||||
Text(
|
||||
text = marmotKeyPackageBundle.id,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = marmotKeyPackageBundle.createdAt.toFormattedTimeAndDateString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
onNavigateToRoute.invoke(
|
||||
ImplementationPendingRoute("Delete Key Package")
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"Delete"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyPackageManagementUIState.Loading -> {
|
||||
LoadingDataIndicator()
|
||||
|
||||
LaunchedEffect(true) {
|
||||
keyPackageManagementViewModel.loadNostrFeed()
|
||||
}
|
||||
}
|
||||
KeyPackageManagementUIState.NotFound -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("We couldn't find the local profile. Please try again later.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UnannouncedProfileScreenPreview() {
|
||||
_root_ide_package_.at.torch.compose.ui.theme.TorchTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
KeyPackageManagementScreen(
|
||||
initialActiveProfileUIState = KeyPackageManagementUIState.Loaded(
|
||||
keyPackageBundles = emptyList()
|
||||
),
|
||||
activeUserPublicKey = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||
nostrEventId = "",
|
||||
marmotRepository = MarmotRepository.NO_OP_MARMOT_KEY_PACKAGE_BUNDLE,
|
||||
onNavigateToRoute = {},
|
||||
onNavigateBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,13 @@ import at.torch.compose.ui.composable.ActiveProfileScreen
|
||||
import at.torch.compose.ui.composable.ChatRoomCreationScreen
|
||||
import at.torch.compose.ui.composable.ChatRoomDetailScreen
|
||||
import at.torch.compose.ui.composable.HomeScreen
|
||||
import at.torch.compose.ui.composable.KeyPackageManagementScreen
|
||||
import at.torch.compose.ui.composable.NostrEventDetailScreen
|
||||
import at.torch.compose.ui.composable.ShareProfileScreen
|
||||
import at.torch.compose.ui.composable.UnsyncedProfileScreen
|
||||
import at.torch.compose.ui.composable.navigation.routes.ActiveProfileRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.KeyPackageManagementRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.LoadingRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute
|
||||
@@ -490,6 +492,23 @@ fun TorchNavHost(
|
||||
}
|
||||
)
|
||||
}
|
||||
composable<KeyPackageManagementRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<KeyPackageManagementRoute>()
|
||||
|
||||
KeyPackageManagementScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
nostrEventId = route.nostrEventId,
|
||||
marmotRepository = databaseMarmotRepository,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToRoute = { route ->
|
||||
navController.navigate(
|
||||
route = route
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
composable<at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute>()
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package at.torch.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class KeyPackageManagementRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val nostrEventId: String,
|
||||
): Route()
|
||||
@@ -112,7 +112,7 @@ class CreateProfileViewModel(
|
||||
biography = createProfileFormState.biographyField.textFieldState.text.toString()
|
||||
)
|
||||
|
||||
marmotRepository.saveMarmotKeyPackageBundle(
|
||||
marmotRepository.publishMarmotKeyPackageBundle(
|
||||
publicKey = pubkey,
|
||||
nsecPassword = localKeyManager.nsecPassword()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package at.torch.compose.ui.view.model
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.repository.MarmotRepository
|
||||
import at.torch.compose.ui.view.state.KeyPackageManagementUIState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class KeyPackageManagementViewModel(
|
||||
val activeUserPublicKey: HexKey,
|
||||
val nostrEventId: HexKey,
|
||||
initialActiveProfileUIState: KeyPackageManagementUIState,
|
||||
val marmotRepository: MarmotRepository
|
||||
): ViewModel() {
|
||||
var keyPackageManagementUIState: KeyPackageManagementUIState by mutableStateOf(initialActiveProfileUIState)
|
||||
private set
|
||||
|
||||
fun retryLoad() {
|
||||
keyPackageManagementUIState = KeyPackageManagementUIState.Loading
|
||||
loadNostrFeed()
|
||||
}
|
||||
|
||||
fun loadNostrFeed() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
keyPackageManagementUIState = KeyPackageManagementUIState.Loaded(
|
||||
keyPackageBundles = marmotRepository.getMarmotKeyPackageBundles(activeUserPublicKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun publishNewKeyPackage() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
marmotRepository.publishMarmotKeyPackageBundle(
|
||||
publicKey = activeUserPublicKey,
|
||||
nsecPassword = "" // TODO: Implement nsecPassword logic...
|
||||
)
|
||||
|
||||
keyPackageManagementUIState = KeyPackageManagementUIState.Loaded(
|
||||
keyPackageBundles = marmotRepository.getMarmotKeyPackageBundles(activeUserPublicKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "KeyPackageManagementViewModel"
|
||||
|
||||
fun factory(
|
||||
nostrEventId: HexKey,
|
||||
activeUserPublicKey: HexKey,
|
||||
initialActiveProfileUIState: KeyPackageManagementUIState = KeyPackageManagementUIState.Loading,
|
||||
marmotRepository: MarmotRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
KeyPackageManagementViewModel(
|
||||
nostrEventId = nostrEventId,
|
||||
initialActiveProfileUIState = initialActiveProfileUIState,
|
||||
marmotRepository = marmotRepository,
|
||||
activeUserPublicKey = activeUserPublicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package at.torch.compose.ui.view.state
|
||||
|
||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||
|
||||
sealed interface KeyPackageManagementUIState {
|
||||
data class Loaded(
|
||||
val keyPackageBundles: List<MarmotKeyPackageBundle>
|
||||
): KeyPackageManagementUIState
|
||||
|
||||
data object Error: KeyPackageManagementUIState
|
||||
data object NotFound: KeyPackageManagementUIState
|
||||
data object Loading: KeyPackageManagementUIState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user