From 9c9d9ae88246f36f7dc36595442290d96dae75b1 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 15 Jul 2026 07:32:39 +0700 Subject: [PATCH] clean up chat screen --- .../androidMain/kotlin/su/reya/coop/App.kt | 47 ++++++++++++------- .../su/reya/coop/screens/chat/ChatScreen.kt | 41 +++++----------- .../commonMain/kotlin/su/reya/coop/Room.kt | 2 +- .../coop/viewmodel/ChatScreenViewModel.kt | 20 ++++++-- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt index c5ae4df..7af97d1 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt @@ -6,11 +6,14 @@ import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme @@ -24,6 +27,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.util.Consumer import androidx.lifecycle.ViewModel @@ -230,26 +235,34 @@ fun App( NewIdentityScreen(accountViewModel) } entry { key -> - val factory = remember(key) { - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return ChatScreenViewModel( - key.id, - key.screening, - accountRepository, - chatRepository - ) as T + val initialRoom = remember(key.id) { chatRepository.getChatRoom(key.id) } + + if (initialRoom != null) { + val factory = remember(initialRoom) { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return ChatScreenViewModel( + initialRoom, + key.screening, + accountRepository, + chatRepository + ) as T + } } } + ChatScreen( + viewModel( + key = key.id.toString(), + factory = factory + ), + accountViewModel + ) + } else { + // Handle the rare case where the room isn't in DB (e.g., invalid deep link) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Room not found") + } } - ChatScreen( - viewModel( - key = key.id.toString(), - factory = factory - ), - accountViewModel - ) } entry { NewChatScreen(accountViewModel, chatViewModel) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt index 1e7fc78..cf36094 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatScreen.kt @@ -94,12 +94,11 @@ import rust.nostr.sdk.UnsignedEvent import su.reya.coop.LocalNavigator import su.reya.coop.LocalProfileCache import su.reya.coop.LocalSnackbarHostState -import su.reya.coop.Room import su.reya.coop.RoomUiState import su.reya.coop.Screen +import su.reya.coop.flow import su.reya.coop.formatAsGroup import su.reya.coop.shared.Avatar -import su.reya.coop.uiStateFlow import su.reya.coop.viewmodel.AccountViewModel import su.reya.coop.viewmodel.ChatScreenViewModel @@ -118,25 +117,11 @@ fun ChatScreen( val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val id = viewModel.id val currentUser by viewModel.currentUser.collectAsStateWithLifecycle() - val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() - val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } } + val pubkey = currentUser?.publicKey - // Show empty screen - if (room == null) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = "Something went wrong.", - style = MaterialTheme.typography.titleMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface - ) - } - return - } + val room by viewModel.room.collectAsStateWithLifecycle() + val roomState by room.flow(profileCache, pubkey).collectAsStateWithLifecycle(RoomUiState()) val loading = viewModel.loading val newOtherMessages = viewModel.newOtherMessages @@ -146,9 +131,6 @@ fun ChatScreen( val groupedMessages = remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } } - val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey) - .collectAsStateWithLifecycle(RoomUiState()) - var text by remember { mutableStateOf("") } var selectedMessage by remember { mutableStateOf?>(null) } var replyingTo by remember { mutableStateOf(null) } @@ -193,9 +175,7 @@ fun ChatScreen( } } - Box( - modifier = Modifier.fillMaxSize() - ) { + Box(modifier = Modifier.fillMaxSize()) { Scaffold( modifier = Modifier.blur(blurAmount), contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime), @@ -207,7 +187,7 @@ fun ChatScreen( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { - room?.members?.firstOrNull()?.let { pubkey -> + room.members.firstOrNull()?.let { pubkey -> navigator.navigate(Screen.Profile(pubkey.toBech32())) } } @@ -265,7 +245,7 @@ fun ChatScreen( .padding(bottom = innerPadding.calculateBottomPadding()) ) { if (requireScreening) { - room?.let { ScreenerCard(accountViewModel, it) } + ScreenerCard(accountViewModel, room) } when (messages.isNotEmpty()) { @@ -283,8 +263,7 @@ fun ChatScreen( items = messagesInGroup, key = { it.ensureId().id()?.toHex()!! } ) { event -> - val model = - rememberMessageModel(event, currentUser?.publicKey) + val model = rememberMessageModel(event, pubkey) val replyPreview = remember(model.replyEventIds, messages.size) { @@ -298,7 +277,9 @@ fun ChatScreen( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(2.dp) ) { - replyPreview?.let { ReplyPreview(it, model.isMine) } + replyPreview?.let { + ReplyPreview(it, model.isMine) + } ChatMessage( model = model, modifier = Modifier.graphicsLayer { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt index 2a01f29..b49d379 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt @@ -71,7 +71,7 @@ data class RoomUiState( val isGroup: Boolean = false ) -fun Room.uiStateFlow( +fun Room.flow( profileCache: ProfileCache, currentUser: PublicKey? = null ): Flow { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt index bec9f34..a8f2f76 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt @@ -7,30 +7,40 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import rust.nostr.sdk.EventId import rust.nostr.sdk.UnsignedEvent -import su.reya.coop.Profile import su.reya.coop.Room import su.reya.coop.repository.AccountRepository import su.reya.coop.repository.ChatRepository import su.reya.coop.roomId class ChatScreenViewModel( - val id: Long, + initialRoom: Room, screening: Boolean, accountRepository: AccountRepository, private val chatRepository: ChatRepository, ) : ViewModel(), ErrorHost by chatRepository { - val currentUser: StateFlow = accountRepository.currentUserProfile - val chatRooms: StateFlow> = chatRepository.chatRooms - var loading by mutableStateOf(true) var newOtherMessages by mutableIntStateOf(0) var requireScreening by mutableStateOf(screening) val messages = mutableStateListOf() + val currentUser = accountRepository.currentUserProfile + val id = initialRoom.id + + val room: StateFlow = chatRepository.chatRooms + .map { rooms -> rooms.find { it.id == id } ?: initialRoom } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = initialRoom + ) + init { loadMessages() connect()