Boilerplate for inviting new member to chat

This commit is contained in:
Kgothatso Ngako
2026-07-10 16:27:18 +02:00
parent adfe49f725
commit 23d9e92675
10 changed files with 868 additions and 4 deletions

View File

@@ -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 = {}
)
}
}
}

View File

@@ -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 = {}
)
}
}

View File

@@ -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 = {}
)
}
}
}

View File

@@ -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<ChatRoomDetailRoute>()
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<SearchMemberToAddToChatRoomRoute> { backStackEntry ->
val route = backStackEntry.toRoute<SearchMemberToAddToChatRoomRoute>()
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<AddMemberToChatRoomConfirmationRoute> { backStackEntry ->
val route = backStackEntry.toRoute<AddMemberToChatRoomConfirmationRoute>()
AddMemberToChatRoomConfirmationScreen(
activeUserPublicKey = route.activeUserPublicKey,
chatRoomId = route.chatRoomId,
relayHint = route.relayHint,

View File

@@ -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()

View File

@@ -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()

View File

@@ -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<Boolean> = 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
)
}
}
}
}

View File

@@ -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<Boolean> = 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
)
}
}
}
}

View File

@@ -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
}

View File

@@ -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
}