diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt new file mode 100644 index 00000000..dc435cce --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt @@ -0,0 +1,205 @@ +package at.torch.compose.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.navigationBarsPadding +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.Mic +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.BottomAppBarDefaults +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.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.runtime.key +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 at.torch.compose.database.model.ChatRoom +import at.torch.compose.database.model.intermdiate.LocalChatRoom +import at.torch.compose.repository.ChatRepository +import at.torch.compose.repository.NostrRepository +import at.torch.compose.ui.composable.navigation.routes.Route +import at.torch.compose.ui.composable.widgets.LoadingDataIndicator +import at.torch.compose.ui.composable.widgets.profile.ProfileColor +import at.torch.compose.ui.theme.TorchTheme +import at.torch.compose.ui.view.model.AddMemberToChatRoomConfirmationViewModel +import at.torch.compose.ui.view.state.AddMemberToChatRoomConfirmationUIState +import at.torch.compose.ui.view.state.ChatRoomDetailUIState +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) +@Composable +fun AddMemberToChatRoomConfirmationScreen( + activeUserPublicKey: HexKey, + chatRoomId: String, + relayHint: String?, + initialChatRoomDetailUIState: AddMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Loading, + nostrRepository: NostrRepository, + chatRepository: ChatRepository, + onNavigateToChat: (Route) -> Unit, +) { + val addMemberToChatRoomConfirmationViewModel: AddMemberToChatRoomConfirmationViewModel = viewModel( + factory = AddMemberToChatRoomConfirmationViewModel.factory( + chatRoomId = chatRoomId, + relayHint = relayHint, + initialAddMemberToChatRoomConfirmationUIState = initialChatRoomDetailUIState, + nostrRepository = nostrRepository, + chatRepository = chatRepository, + activeUserPublicKey = activeUserPublicKey + ) + ) + + when (val addMemberToChatRoomConfirmationUIState = addMemberToChatRoomConfirmationViewModel.addMemberToChatRoomConfirmationUIState) { + is AddMemberToChatRoomConfirmationUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = addMemberToChatRoomConfirmationUIState.message, + ) + } + } + is AddMemberToChatRoomConfirmationUIState.Loaded -> { + val textFieldState = rememberTextFieldState() + + val chatRoomDetailMessageListViewModel: at.torch.compose.ui.view.model.ChatRoomDetailMessageListViewModel = viewModel( + factory = at.torch.compose.ui.view.model.ChatRoomDetailMessageListViewModel.factory( + localChatRoom = addMemberToChatRoomConfirmationUIState.localChatRoom, + nostrRepository = nostrRepository, + chatRepository = chatRepository + ) + ) + + Scaffold( + topBar = { + TopAppBar( + title = { + val title = if (addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject != null) { + "Invite new member to ${addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject}" + } else { + "Invite new member to chat" + } + Text( + text = title, + ) + }, + actions = { + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).fillMaxSize() + ) { + Column( + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + Text("Render contact List") + } + + key(true) { + chatRoomDetailMessageListViewModel.initiate() + } + + // TODO: Check that we have direct message relays for this user... + } + } + } + AddMemberToChatRoomConfirmationUIState.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 (initialChatRoomDetailUIState == AddMemberToChatRoomConfirmationUIState.Loading) { + addMemberToChatRoomConfirmationViewModel.initiateChatRoomDetail() + } + } +} + +@Preview +@Composable +private fun ChatRoomDetailScreenPreview() { + TorchTheme { + Surface( + modifier = Modifier.fillMaxSize() + ) { + AddMemberToChatRoomConfirmationScreen( + activeUserPublicKey = "", + chatRoomId = "publicKey", + relayHint = null, + initialChatRoomDetailUIState = AddMemberToChatRoomConfirmationUIState.Loaded( + localChatRoom = LocalChatRoom( + chatRoom = ChatRoom( + id = "", + userPublicKey = "", + subject = "Message title", + initialGiftWrapPayloadId = "sdfaer", + mlsGroupState = null + ), + ) + ), + nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, + chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, + onNavigateToChat = {} + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt index 895621f8..89b2d678 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt @@ -42,6 +42,7 @@ import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.repository.ChatRepository import at.torch.compose.repository.NostrRepository import at.torch.compose.ui.composable.navigation.routes.Route +import at.torch.compose.ui.composable.navigation.routes.SearchMemberToAddToChatRoomRoute import at.torch.compose.ui.composable.widgets.LoadingDataIndicator import at.torch.compose.ui.theme.TorchTheme import at.torch.compose.ui.view.model.ChatRoomDetailViewModel @@ -57,7 +58,8 @@ fun ChatRoomDetailScreen( initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading, nostrRepository: NostrRepository, chatRepository: ChatRepository, - onNavigateToChat: (Route) -> Unit, + onNavigateToRouteAndPopUpInclusive: (Route) -> Unit, + onNavigateToRoute: (Route) -> Unit, ) { val chatRoomDetailViewModel: ChatRoomDetailViewModel = viewModel( factory = ChatRoomDetailViewModel.factory( @@ -104,7 +106,14 @@ fun ChatRoomDetailScreen( actions = { IconButton( onClick = { - // TODO: Navigate to the invite user screen... + // Navigate to the invite user screen... + onNavigateToRoute.invoke( + SearchMemberToAddToChatRoomRoute( + chatRoomId = chatRoomId, + activeUserPublicKey = activeUserPublicKey, + relayHint = relayHint + ) + ) } ) { Icon( @@ -298,7 +307,7 @@ fun ChatRoomDetailScreen( LaunchedEffect(true) { chatRoomDetailViewModel.initiateNewChat( - onNavigateToChat + onNavigateToRouteAndPopUpInclusive ) } } @@ -336,7 +345,8 @@ private fun ChatRoomDetailScreenPreview() { ), nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, - onNavigateToChat = {} + onNavigateToRouteAndPopUpInclusive = {}, + onNavigateToRoute = {} ) } } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt new file mode 100644 index 00000000..e19f5854 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/SearchMemberToAddToChatRoomScreen.kt @@ -0,0 +1,218 @@ +package at.torch.compose.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.navigationBarsPadding +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.Mic +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.BottomAppBarDefaults +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.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.runtime.key +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 at.torch.compose.database.model.ChatRoom +import at.torch.compose.database.model.intermdiate.LocalChatRoom +import at.torch.compose.repository.ChatRepository +import at.torch.compose.repository.NostrRepository +import at.torch.compose.ui.composable.navigation.routes.AddMemberToChatRoomConfirmationRoute +import at.torch.compose.ui.composable.navigation.routes.Route +import at.torch.compose.ui.composable.widgets.LoadingDataIndicator +import at.torch.compose.ui.theme.TorchTheme +import at.torch.compose.ui.view.model.ChatRoomDetailMessageListViewModel +import at.torch.compose.ui.view.model.SearchMemberToAddToChatRoomViewModel +import at.torch.compose.ui.view.state.ChatRoomDetailUIState +import at.torch.compose.ui.view.state.SearchMemberToAddToChatRoomUIState +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) +@Composable +fun SearchMemberToAddToChatRoomScreen( + activeUserPublicKey: HexKey, + chatRoomId: String, + relayHint: String?, + initialSearchMemberToAddToChatRoomUIState: SearchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Loading, + nostrRepository: NostrRepository, + chatRepository: ChatRepository, + onNavigateToRoute: (Route) -> Unit, +) { + val searchMemberToAddToChatRoomViewModel: SearchMemberToAddToChatRoomViewModel = viewModel( + factory = SearchMemberToAddToChatRoomViewModel.factory( + chatRoomId = chatRoomId, + relayHint = relayHint, + initialSearchMemberToAddToChatRoomUIState = initialSearchMemberToAddToChatRoomUIState, + nostrRepository = nostrRepository, + chatRepository = chatRepository, + activeUserPublicKey = activeUserPublicKey + ) + ) + + when (val searchMemberToAddToChatRoomUIState = searchMemberToAddToChatRoomViewModel.searchMemberToAddToChatRoomUIState) { + is SearchMemberToAddToChatRoomUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = searchMemberToAddToChatRoomUIState.message, + ) + } + } + is SearchMemberToAddToChatRoomUIState.Loaded -> { + val textFieldState = rememberTextFieldState() + + val chatRoomDetailMessageListViewModel: ChatRoomDetailMessageListViewModel = viewModel( + factory = ChatRoomDetailMessageListViewModel.factory( + localChatRoom = searchMemberToAddToChatRoomUIState.localChatRoom, + nostrRepository = nostrRepository, + chatRepository = chatRepository + ) + ) + + Scaffold( + topBar = { + TopAppBar( + title = { + val title = if (searchMemberToAddToChatRoomUIState.localChatRoom.chatRoom.subject != null) { + "Invite new member to ${searchMemberToAddToChatRoomUIState.localChatRoom.chatRoom.subject}" + } else { + "Invite new member to chat" + } + Text( + text = title, + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).fillMaxSize() + ) { + Column( + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + key(true) { + Button( + onClick = { + onNavigateToRoute.invoke( + AddMemberToChatRoomConfirmationRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + relayHint = relayHint + ) + ) + } + ) { + Text("Contact List TODO") + } + } + } + + key(true) { + chatRoomDetailMessageListViewModel.initiate() + } + + // TODO: Check that we have direct message relays for this user... + } + } + } + SearchMemberToAddToChatRoomUIState.Loading -> { + Column( + modifier = Modifier.fillMaxWidth().padding( + 20.dp + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + + Spacer( + modifier = Modifier.weight(1f) + ) + Text( + text = "Search member functionality", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + + Text( + text = "Search message functionality will be here.", + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center + ) + + LoadingDataIndicator( + fillScreen = false + ) + + Spacer( + modifier = Modifier.weight(2f) + ) + } + } + } + + LaunchedEffect(true) { + if (initialSearchMemberToAddToChatRoomUIState == SearchMemberToAddToChatRoomUIState.Loading) { + searchMemberToAddToChatRoomViewModel.initiateChatRoomDetail() + } + } +} + +@Preview +@Composable +private fun ChatRoomDetailScreenPreview() { + TorchTheme { + Surface( + modifier = Modifier.fillMaxSize() + ) { + SearchMemberToAddToChatRoomScreen( + activeUserPublicKey = "", + chatRoomId = "publicKey", + relayHint = null, + initialSearchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Loaded( + localChatRoom = LocalChatRoom( + chatRoom = ChatRoom( + id = "", + userPublicKey = "", + subject = "Message title", + initialGiftWrapPayloadId = "sdfaer", + mlsGroupState = null + ), + ) + ), + nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, + chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, + onNavigateToRoute = {} + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt index b57d6476..e855b136 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt @@ -18,12 +18,14 @@ import at.torch.compose.database.repository.DatabaseMarmotRepository import at.torch.compose.database.repository.DatabaseNostrRepository import at.torch.compose.database.repository.DatabaseSearchRepository import at.torch.compose.ui.composable.ActiveProfileScreen +import at.torch.compose.ui.composable.AddMemberToChatRoomConfirmationScreen 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.ImplementationPendingScreen import at.torch.compose.ui.composable.KeyPackageManagementScreen import at.torch.compose.ui.composable.NostrEventDetailScreen +import at.torch.compose.ui.composable.SearchMemberToAddToChatRoomScreen import at.torch.compose.ui.composable.SearchResultScreen import at.torch.compose.ui.composable.SearchScreen import at.torch.compose.ui.composable.ShareProfileScreen @@ -37,6 +39,7 @@ import at.torch.compose.ui.composable.UnsignedProfileScreen import at.torch.compose.ui.composable.UnsyncedProfileScreen import at.torch.compose.ui.composable.WriteNewNoteScreen import at.torch.compose.ui.composable.navigation.routes.ActiveProfileRoute +import at.torch.compose.ui.composable.navigation.routes.AddMemberToChatRoomConfirmationRoute import at.torch.compose.ui.composable.navigation.routes.BlankRoute import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute @@ -47,6 +50,7 @@ import at.torch.compose.ui.composable.navigation.routes.KeyPackageManagementRout import at.torch.compose.ui.composable.navigation.routes.LandingRoute 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.SearchMemberToAddToChatRoomRoute import at.torch.compose.ui.composable.navigation.routes.SearchResultRoute import at.torch.compose.ui.composable.navigation.routes.SearchRoute import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute @@ -549,6 +553,51 @@ fun TorchNavHost( val route = backStackEntry.toRoute() ChatRoomDetailScreen( + 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() + + SearchMemberToAddToChatRoomScreen( + activeUserPublicKey = route.activeUserPublicKey, + chatRoomId = route.chatRoomId, + relayHint = route.relayHint, + nostrRepository = databaseNostrRepository, + chatRepository = databaseChatRepository, + onNavigateToRoute = { chatRoomDetailRoute -> + navController.navigate( + route = chatRoomDetailRoute + ) { + popUpTo(route) { + inclusive = true + } + } + }, + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + + AddMemberToChatRoomConfirmationScreen( activeUserPublicKey = route.activeUserPublicKey, chatRoomId = route.chatRoomId, relayHint = route.relayHint, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/routes/AddMemberToChatRoomConfirmationRoute.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/routes/AddMemberToChatRoomConfirmationRoute.kt new file mode 100755 index 00000000..cf7f33d5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/routes/AddMemberToChatRoomConfirmationRoute.kt @@ -0,0 +1,10 @@ +package at.torch.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +data class AddMemberToChatRoomConfirmationRoute( + 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/at/torch/compose/ui/composable/navigation/routes/SearchMemberToAddToChatRoomRoute.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/routes/SearchMemberToAddToChatRoomRoute.kt new file mode 100755 index 00000000..85202bc5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/routes/SearchMemberToAddToChatRoomRoute.kt @@ -0,0 +1,10 @@ +package at.torch.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +data class SearchMemberToAddToChatRoomRoute( + 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/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt new file mode 100644 index 00000000..a4ab892b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt @@ -0,0 +1,166 @@ +package at.torch.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 at.torch.compose.repository.ChatRepository +import at.torch.compose.repository.NostrRepository +import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute +import at.torch.compose.ui.composable.navigation.routes.Route +import at.torch.compose.ui.view.state.AddMemberToChatRoomConfirmationUIState +import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class AddMemberToChatRoomConfirmationViewModel( + val chatRoomId: String, + val activeUserPublicKey: HexKey, + val relayHint: String?, + initialAddMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState, + val nostrRepository: NostrRepository, + val chatRepository: ChatRepository +): ViewModel() { + + var addMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState by mutableStateOf(initialAddMemberToChatRoomConfirmationUIState) + 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) + + addMemberToChatRoomConfirmationUIState = if (localChatRoom == null) { + // Initiate new chat... and navigate to new chat... + AddMemberToChatRoomConfirmationUIState.Error("Couldn't load chat") + } else { + AddMemberToChatRoomConfirmationUIState.Loaded( + localChatRoom = localChatRoom + ) + } + } + } + + fun initiateNewChat( + onNavigateToChat: (Route) -> Unit + ) { + logger.d("initiateNewChat: $chatRoomId") + // Get ChatRoomById + viewModelScope.launch(Dispatchers.IO) { + + // Check if we have a chat with activeUserPublicKey and chatRoomId (pubkey) as participants + val participants = chatRepository.getAllParticipantsWithPubKey(chatRoomId) + + participants.forEach { participant -> + chatRepository.getChatRoomByIdentifier(participant.chatRoomId)?.let { localChatRoom -> + // TODO: Check that chatRoom is not a group chat... + if (localChatRoom.localParticipants.size == 1) { + // This is a chat with self... + // TODO: navigateToTheChat... + viewModelScope.launch(Dispatchers.Main) { + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = localChatRoom.chatRoom.id, + relayHint = relayHint + ) + ) + } + return@launch + + } else if (localChatRoom.localParticipants.size == 2) { + val activeUserParticipant = localChatRoom.localParticipants.find { localParticipant -> localParticipant.participant.participantPublicKey == activeUserPublicKey } + + if (activeUserParticipant != null) { + // We have found the chat we are looking for... + viewModelScope.launch(Dispatchers.Main) { + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = localChatRoom.chatRoom.id, + relayHint = relayHint + ) + ) + } + return@launch + } + } + } + } + + val profile = chatRepository.getProfileWithPublicKey(chatRoomId) + + if (profile != null) { + logger.d("Create chat with: $profile") + // Check if we have a keyPackageEvent... + val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId) + + if (keyPackageEvent == null) { + addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Missing Key Package") // TODO: Introduce missing key package UI State... + } else { + // Create new chat and invite local + try { + val chatRoomId = chatRepository.createMlsDirectMessage( + userPublicKey = activeUserPublicKey, + peerPublicKey = chatRoomId, + peerKeyPackage = keyPackageEvent, + ) + + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + relayHint = relayHint + ) + ) + } catch (e: Throwable) { + addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Failed to create chat") + + } + } + + } else { + addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Couldn't find profile...") + } + } + } + + + companion object { + private const val TAG = "AddMemberToChatRoomConfirmationViewModel" + + fun factory( + activeUserPublicKey: HexKey, + chatRoomId: String, + relayHint: String?, + initialAddMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Loading, + nostrRepository: NostrRepository, + chatRepository: ChatRepository + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + AddMemberToChatRoomConfirmationViewModel( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + relayHint = relayHint, + initialAddMemberToChatRoomConfirmationUIState = initialAddMemberToChatRoomConfirmationUIState, + nostrRepository = nostrRepository, + chatRepository = chatRepository + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SearchMemberToAddToChatRoomViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SearchMemberToAddToChatRoomViewModel.kt new file mode 100644 index 00000000..05a57a34 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SearchMemberToAddToChatRoomViewModel.kt @@ -0,0 +1,166 @@ +package at.torch.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 at.torch.compose.repository.ChatRepository +import at.torch.compose.repository.NostrRepository +import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute +import at.torch.compose.ui.composable.navigation.routes.Route +import at.torch.compose.ui.view.state.SearchMemberToAddToChatRoomUIState +import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class SearchMemberToAddToChatRoomViewModel( + val chatRoomId: String, + val activeUserPublicKey: HexKey, + val relayHint: String?, + initialSearchMemberToAddToChatRoomUIState: SearchMemberToAddToChatRoomUIState, + val nostrRepository: NostrRepository, + val chatRepository: ChatRepository +): ViewModel() { + + var searchMemberToAddToChatRoomUIState: SearchMemberToAddToChatRoomUIState by mutableStateOf(initialSearchMemberToAddToChatRoomUIState) + 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) + + searchMemberToAddToChatRoomUIState = if (localChatRoom == null) { + // Initiate new chat... and navigate to new chat... + SearchMemberToAddToChatRoomUIState.Error("Failed to load chat") + } else { + SearchMemberToAddToChatRoomUIState.Loaded( + localChatRoom = localChatRoom + ) + } + } + } + + fun initiateNewChat( + onNavigateToChat: (Route) -> Unit + ) { + logger.d("initiateNewChat: $chatRoomId") + // Get ChatRoomById + viewModelScope.launch(Dispatchers.IO) { + + // Check if we have a chat with activeUserPublicKey and chatRoomId (pubkey) as participants + val participants = chatRepository.getAllParticipantsWithPubKey(chatRoomId) + + participants.forEach { participant -> + chatRepository.getChatRoomByIdentifier(participant.chatRoomId)?.let { localChatRoom -> + // TODO: Check that chatRoom is not a group chat... + if (localChatRoom.localParticipants.size == 1) { + // This is a chat with self... + // TODO: navigateToTheChat... + viewModelScope.launch(Dispatchers.Main) { + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = localChatRoom.chatRoom.id, + relayHint = relayHint + ) + ) + } + return@launch + + } else if (localChatRoom.localParticipants.size == 2) { + val activeUserParticipant = localChatRoom.localParticipants.find { localParticipant -> localParticipant.participant.participantPublicKey == activeUserPublicKey } + + if (activeUserParticipant != null) { + // We have found the chat we are looking for... + viewModelScope.launch(Dispatchers.Main) { + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = localChatRoom.chatRoom.id, + relayHint = relayHint + ) + ) + } + return@launch + } + } + } + } + + val profile = chatRepository.getProfileWithPublicKey(chatRoomId) + + if (profile != null) { + logger.d("Create chat with: $profile") + // Check if we have a keyPackageEvent... + val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId) + + if (keyPackageEvent == null) { + searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Missing Key Package") // TODO: Introduce missing key package UI State... + } else { + // Create new chat and invite local + try { + val chatRoomId = chatRepository.createMlsDirectMessage( + userPublicKey = activeUserPublicKey, + peerPublicKey = chatRoomId, + peerKeyPackage = keyPackageEvent, + ) + + onNavigateToChat.invoke( + ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + relayHint = relayHint + ) + ) + } catch (e: Throwable) { + searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Failed to create chat") + + } + } + + } else { + searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Couldn't find profile...") + } + } + } + + + companion object { + private const val TAG = "SearchMemberToAddToChatRoomViewModel" + + fun factory( + activeUserPublicKey: HexKey, + chatRoomId: String, + relayHint: String?, + initialSearchMemberToAddToChatRoomUIState: SearchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Loading, + nostrRepository: NostrRepository, + chatRepository: ChatRepository + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + SearchMemberToAddToChatRoomViewModel( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + relayHint = relayHint, + initialSearchMemberToAddToChatRoomUIState = initialSearchMemberToAddToChatRoomUIState, + nostrRepository = nostrRepository, + chatRepository = chatRepository + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt new file mode 100755 index 00000000..555a6ac3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt @@ -0,0 +1,15 @@ +package at.torch.compose.ui.view.state + +import at.torch.compose.database.model.intermdiate.LocalChatRoom + +sealed interface AddMemberToChatRoomConfirmationUIState { + data class Loaded( + val localChatRoom: LocalChatRoom + ): AddMemberToChatRoomConfirmationUIState + + data class Error( + val message: String + ): AddMemberToChatRoomConfirmationUIState + data object Loading: AddMemberToChatRoomConfirmationUIState +} + diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/SearchMemberToAddToChatRoomUIState.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/SearchMemberToAddToChatRoomUIState.kt new file mode 100755 index 00000000..29eb21f2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/SearchMemberToAddToChatRoomUIState.kt @@ -0,0 +1,15 @@ +package at.torch.compose.ui.view.state + +import at.torch.compose.database.model.intermdiate.LocalChatRoom + +sealed interface SearchMemberToAddToChatRoomUIState { + data class Loaded( + val localChatRoom: LocalChatRoom + ): SearchMemberToAddToChatRoomUIState + + data class Error( + val message: String + ): SearchMemberToAddToChatRoomUIState + data object Loading: SearchMemberToAddToChatRoomUIState +} +