diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt new file mode 100644 index 00000000..e72b1e4b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt @@ -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 = {} + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt index f5c9e16e..151a86de 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt @@ -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 ) ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt index e8b96ed9..38fdb680 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt @@ -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 { backStackEntry -> + val route = backStackEntry.toRoute() + + 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 { backStackEntry -> val route = backStackEntry.toRoute() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/AddArtifactRoute.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/AddArtifactRoute.kt new file mode 100755 index 00000000..e6cfcca0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/AddArtifactRoute.kt @@ -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() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/AddArtifactViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/AddArtifactViewModel.kt new file mode 100644 index 00000000..fb2c4837 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/AddArtifactViewModel.kt @@ -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 = 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 + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/AddArtifactUIState.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/AddArtifactUIState.kt new file mode 100755 index 00000000..80395d82 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/AddArtifactUIState.kt @@ -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 +} +