Add artifact UI flow

This commit is contained in:
Kgothatso Ngako
2026-07-20 02:30:15 +02:00
parent 3a830d77d6
commit 499bd54704
6 changed files with 458 additions and 3 deletions

View File

@@ -0,0 +1,312 @@
package press.mantra.compose.ui.composable
import androidx.compose.foundation.background
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DriveFileRenameOutline
import androidx.compose.material.icons.filled.Label
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.LocalOffer
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Title
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.graphics.Color
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 press.mantra.compose.database.model.ChatRoom
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.NostrRepository
import press.mantra.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import press.mantra.compose.ui.composable.navigation.routes.Route
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
import press.mantra.compose.ui.theme.TorchTheme
import press.mantra.compose.ui.view.state.ChatRoomMessagingUIState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import press.mantra.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import press.mantra.compose.ui.view.model.AddArtifactViewModel
import press.mantra.compose.ui.view.state.AddArtifactUIState
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun AddArtifactScreen(
activeUserPublicKey: HexKey,
chatRoomId: String,
relayHint: String?,
initialAddArtifactUIState: AddArtifactUIState = AddArtifactUIState.Loading,
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
onNavigateToRouteAndPopUpInclusive: (Route) -> Unit,
onNavigateToRoute: (Route) -> Unit,
) {
val addArtifactViewModel: AddArtifactViewModel = viewModel(
factory = AddArtifactViewModel.factory(
chatRoomId = chatRoomId,
relayHint = relayHint,
initialAddArtifactUIState = initialAddArtifactUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository,
activeUserPublicKey = activeUserPublicKey
)
)
when (val addArtifactUIState = addArtifactViewModel.addArtifactUIState) {
is AddArtifactUIState.Error -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = addArtifactUIState.message,
)
}
}
is AddArtifactUIState.Loaded -> {
val nameFieldState = rememberTextFieldState()
val urlFieldState = rememberTextFieldState()
val versionLabelFieldState = rememberTextFieldState("1.0")
Scaffold(
topBar = {
TopAppBar(
title = {
addArtifactUIState.localChatRoom.RenderChatRoomTitleText()
},
actions = {
}
)
},
bottomBar = {
BottomAppBar(
actions = {},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
addArtifactViewModel.addArtifact(
name = nameFieldState.text.toString(),
url = urlFieldState.text.toString(),
versionLabel = versionLabelFieldState.text.toString(),
onSuccess = {
onNavigateToRoute.invoke(
ImplementationPendingRoute("Add Artifact")
)
},
onFailure = {
onNavigateToRoute.invoke(
ImplementationPendingRoute("Failed Artifact")
)
}
)
}
) {
Icon(
Icons.Default.Add,
contentDescription = "Add artifact"
)
Text("Add Artifact")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
// TODO: Check that we have direct message relays for this user...
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Add Artifact to the group library")
OutlinedTextField(
modifier = Modifier.fillMaxWidth()
.background(BottomAppBarDefaults.containerColor),
state = nameFieldState,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent
),
leadingIcon = {
Icon(
Icons.Default.DriveFileRenameOutline,
contentDescription = "Name of the artifact"
)
},
label = {
Text(
text = "Name of Artifact"
)
},
placeholder = {
Text(
text = "eg. To Kill a Mocking Bird"
)
},
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth()
.background(BottomAppBarDefaults.containerColor),
state = urlFieldState,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent
),
leadingIcon = {
Icon(
Icons.Default.Link,
contentDescription = "Link to the artifact"
)
},
label = {
Text(
text = "Url"
)
},
placeholder = {
Text(
text = "eg. https://harper.com/2-kill-Bird"
)
},
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth()
.background(BottomAppBarDefaults.containerColor),
state = versionLabelFieldState,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent
),
leadingIcon = {
Icon(
Icons.Default.LocalOffer,
contentDescription = "Verison label"
)
},
label = {
Text(
text = "Initial Version Label"
)
},
placeholder = {
Text(
text = "eg. First Edition"
)
},
)
// TODO: Add Visibility
}
}
}
}
AddArtifactUIState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth().padding(
20.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Spacer(
modifier = Modifier.weight(1f)
)
Text(
text = "Direct Message Detail",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = "Direct message functionality will be here.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
LoadingDataIndicator(
fillScreen = false
)
Spacer(
modifier = Modifier.weight(2f)
)
}
}
}
LaunchedEffect(true) {
if (initialAddArtifactUIState == AddArtifactUIState.Loading) {
addArtifactViewModel.initiateChatRoomDetail()
}
}
}
@Preview
@Composable
private fun ChatRoomMessagingScreenPreview() {
TorchTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
AddArtifactScreen(
activeUserPublicKey = "",
chatRoomId = "publicKey",
relayHint = null,
initialAddArtifactUIState = AddArtifactUIState.Loaded(
localChatRoom = LocalChatRoom(
chatRoom = ChatRoom(
id = "",
userPublicKey = "",
subject = "Message title",
description = "See something. Say somethin",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),
)
),
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
onNavigateToRouteAndPopUpInclusive = {},
onNavigateToRoute = {}
)
}
}
}

View File

@@ -56,6 +56,7 @@ import press.mantra.compose.ui.theme.TorchTheme
import press.mantra.compose.ui.view.model.ChatRoomDetailViewModel
import press.mantra.compose.ui.view.state.ChatRoomDetailUIState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import press.mantra.compose.ui.composable.navigation.routes.AddArtifactRoute
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -135,7 +136,7 @@ fun ChatRoomDetailScreen(
}
item {
Text("No artifacts exists in this group.")
Text("No artifacts exists in this groups library.")
}
item {
@@ -143,8 +144,10 @@ fun ChatRoomDetailScreen(
onClick = {
// Navigate to the invite user screen...
onNavigateToRoute.invoke(
ImplementationPendingRoute(
"Add new artifact"
AddArtifactRoute(
chatRoomId = chatRoomId,
activeUserPublicKey = activeUserPublicKey,
relayHint = relayHint
)
)
}

View File

@@ -86,6 +86,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import press.mantra.compose.ui.composable.AddArtifactScreen
import press.mantra.compose.ui.composable.navigation.routes.AddArtifactRoute
@Composable
fun MantraNavHost(
@@ -641,6 +643,31 @@ fun MantraNavHost(
},
)
}
composable<AddArtifactRoute> { backStackEntry ->
val route = backStackEntry.toRoute<AddArtifactRoute>()
AddArtifactScreen(
activeUserPublicKey = route.activeUserPublicKey,
chatRoomId = route.chatRoomId,
relayHint = route.relayHint,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
onNavigateToRouteAndPopUpInclusive = { chatRoomDetailRoute ->
navController.navigate(
route = chatRoomDetailRoute
) {
popUpTo(route) {
inclusive = true
}
}
},
onNavigateToRoute = { actionRoute ->
navController.navigate(
route = actionRoute
)
}
)
}
composable<SearchRoute> { backStackEntry ->
val route = backStackEntry.toRoute<SearchRoute>()

View File

@@ -0,0 +1,10 @@
package press.mantra.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
data class AddArtifactRoute(
val activeUserPublicKey: String,
val chatRoomId: String, // TODO: have this as a publicKey
val relayHint: String?
): Route()

View File

@@ -0,0 +1,87 @@
package press.mantra.compose.ui.view.model
import androidx.compose.runtime.MutableState
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 press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.NostrRepository
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
import press.mantra.compose.ui.view.state.AddArtifactUIState
class AddArtifactViewModel(
val chatRoomId: String,
val activeUserPublicKey: HexKey,
val relayHint: String?,
initialAddArtifactUIState: AddArtifactUIState,
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository
): ViewModel() {
var addArtifactUIState: AddArtifactUIState by mutableStateOf(initialAddArtifactUIState)
private set
private val logger = Logger.withTag(TAG)
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
fun initiateChatRoomDetail() {
logger.d("compressed (most likely chat room): $chatRoomId")
// Get ChatRoomById
viewModelScope.launch(Dispatchers.IO) {
val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId)
addArtifactUIState = if (localChatRoom == null) {
// Initiate new chat... and navigate to new chat...
AddArtifactUIState.Error("Couldn't find the chat room")
} else {
AddArtifactUIState.Loaded(
localChatRoom = localChatRoom
)
}
}
}
fun addArtifact(
name: String,
url: String,
versionLabel: String,
onSuccess: () -> Unit,
onFailure: () -> Unit
) {
}
companion object {
private const val TAG = "AddArtifactViewModel"
fun factory(
activeUserPublicKey: HexKey,
chatRoomId: String,
relayHint: String?,
initialAddArtifactUIState: AddArtifactUIState = AddArtifactUIState.Loading,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
AddArtifactViewModel(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = chatRoomId,
relayHint = relayHint,
initialAddArtifactUIState = initialAddArtifactUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository
)
}
}
}
}

View File

@@ -0,0 +1,16 @@
package press.mantra.compose.ui.view.state
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
sealed interface AddArtifactUIState {
data class Loaded(
val localChatRoom: LocalChatRoom
): AddArtifactUIState
data class Error(
val message: String
): AddArtifactUIState
data object Loading: AddArtifactUIState
}