diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt index c5ae4df..e9101cd 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.os.Build import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -134,10 +133,6 @@ fun App( else -> expressiveLightColorScheme() } - BackHandler(enabled = backStack.size > 1) { - navigator.goBack() - } - LaunchedEffect(Unit) { launch { accountViewModel.errorEvents.collect { message -> diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt index 4220715..1e97291 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -7,6 +7,7 @@ import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -86,6 +87,7 @@ import coop.composeapp.generated.resources.ic_new_chat import coop.composeapp.generated.resources.ic_qr import coop.composeapp.generated.resources.ic_request import coop.composeapp.generated.resources.ic_scanner +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import rust.nostr.sdk.PublicKey @@ -249,7 +251,9 @@ fun HomeScreen( modifier = Modifier.padding(top = innerPadding.calculateTopPadding()), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - if (!isNotificationEnabled && !isBannerDismissed) { + AnimatedVisibility( + visible = !isNotificationEnabled && !isBannerDismissed, + ) { Surface( modifier = Modifier .fillMaxWidth() @@ -372,10 +376,14 @@ fun HomeScreen( } items(ongoing, key = { it.id }) { room -> - ChatRoom( - room = room, - onClick = { navigator.navigate(Screen.Chat(room.id)) } - ) + Row( + modifier = Modifier.animateItem() + ) { + ChatRoom( + room = room, + onClick = { navigator.navigate(Screen.Chat(room.id)) } + ) + } } } } @@ -620,13 +628,16 @@ fun NewRequests(requests: List) { val firstRoom = requests.getOrNull(0) val secondRoom = requests.getOrNull(1) - val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache) - .collectAsStateWithLifecycle(RoomUiState()) - val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache) - .collectAsStateWithLifecycle(RoomUiState()) + val firstRoomState by remember(firstRoom?.id) { + firstRoom?.uiStateFlow(profileCache) ?: flowOf(RoomUiState()) + }.collectAsStateWithLifecycle(RoomUiState()) + + val secondRoomState by remember(secondRoom?.id) { + (secondRoom ?: firstRoom)?.uiStateFlow(profileCache) ?: flowOf(RoomUiState()) + }.collectAsStateWithLifecycle(RoomUiState()) val supportingText = when { - total == 1 -> { + total == 1 && firstRoom != null -> { val message = firstRoom.lastMessage ?: "" "${firstRoomState.name}: $message" } @@ -695,7 +706,8 @@ fun NewRequests(requests: List) { @Composable fun ChatRoom(room: Room, onClick: () -> Unit) { val profileCache = LocalProfileCache.current - val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState()) + val roomState by remember(room.id) { room.uiStateFlow(profileCache) } + .collectAsStateWithLifecycle(RoomUiState()) ListItem( modifier = Modifier.clickable(onClick = onClick), diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt index c28de53..4ded00f 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt @@ -185,8 +185,8 @@ fun NewChatScreen( ) }, text = { Text("Next") }, - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) } } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt index 50e7c99..213c9b9 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/chat/ChatMessage.kt @@ -1,10 +1,5 @@ package su.reya.coop.screens.chat -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -177,11 +172,7 @@ fun ChatMessage( ) } } - AnimatedVisibility( - visible = isMessageClicked, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { + if (isMessageClicked) { Text( text = model.timestamp, style = MaterialTheme.typography.labelSmall, 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..bda1743 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 @@ -146,8 +146,9 @@ fun ChatScreen( val groupedMessages = remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } } - val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey) - .collectAsStateWithLifecycle(RoomUiState()) + val roomState by remember(id, currentUser?.publicKey) { + (room as Room).uiStateFlow(profileCache, currentUser?.publicKey) + }.collectAsStateWithLifecycle(RoomUiState()) var text by remember { mutableStateOf("") } var selectedMessage by remember { mutableStateOf?>(null) } @@ -295,7 +296,9 @@ fun ChatScreen( } Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .animateItem(), verticalArrangement = Arrangement.spacedBy(2.dp) ) { replyPreview?.let { ReplyPreview(it, model.isMine) } @@ -380,8 +383,10 @@ fun ChatScreen( } else -> { - replyingTo?.let { - ReplyBox(it) { replyingTo = null } + AnimatedVisibility(visible = replyingTo != null) { + replyingTo?.let { + ReplyBox(it) { replyingTo = null } + } } ChatInput( value = text, @@ -420,7 +425,6 @@ fun ChatScreen( } } ) - AnimatedVisibility( visible = selectedMessage != null, enter = fadeIn(), diff --git a/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt b/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt index f26410b..5cbda77 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -21,6 +22,7 @@ import rust.nostr.sdk.PublicKey import rust.nostr.sdk.Tag import rust.nostr.sdk.UnsignedEvent import su.reya.coop.Room +import su.reya.coop.RoomKind import su.reya.coop.nostr.Nostr import su.reya.coop.roomId import su.reya.coop.viewmodel.ErrorHost @@ -38,11 +40,7 @@ class ChatRepository( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ErrorHost by createErrorHost() { private val _state = MutableStateFlow(ChatState()) - val state = _state.stateIn( - scope, - SharingStarted.WhileSubscribed(5000), - ChatState() - ) + val state = _state.asStateFlow() private val _newEvents = MutableSharedFlow( replay = 0, @@ -134,10 +132,19 @@ class ChatRepository( fun refreshChatRooms() { scope.launch(defaultDispatcher) { try { - val rooms = nostr.messages.getChatRooms() ?: emptySet() + val dbRooms = nostr.messages.getChatRooms() ?: emptySet() _state.update { currentState -> val newMap = currentState.rooms.toMutableMap() - rooms.forEach { room -> newMap[room.id] = room } + dbRooms.forEach { dbRoom -> + val existing = newMap[dbRoom.id] + // Only update if the database version is newer or equal + if (existing == null || dbRoom.createdAt.asSecs() >= existing.createdAt.asSecs()) { + // Preserve Ongoing kind if already marked as such in memory + val mergedKind = + if (existing?.kind == RoomKind.Ongoing) RoomKind.Ongoing else dbRoom.kind + newMap[dbRoom.id] = dbRoom.copy(kind = mergedKind) + } + } currentState.copy(rooms = newMap) } } catch (e: CancellationException) { @@ -223,16 +230,24 @@ class ChatRepository( val rooms = currentState.rooms.toMutableMap() val existingRoom = rooms[roomId] + val isFromMe = event.author() == currentUser + val newKind = + if (isFromMe) RoomKind.Ongoing else (existingRoom?.kind ?: RoomKind.Request) + if (existingRoom == null) { // New room discovery - val newRoom = Room.new(event, currentUser, roomId) + val newRoom = Room.new(event, currentUser, roomId).copy(kind = newKind) rooms[newRoom.id] = newRoom } else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) { // Only update preview if message is newer (handles sync/late arrivals) rooms[roomId] = existingRoom.copy( lastMessage = event.content(), - createdAt = event.createdAt() + createdAt = event.createdAt(), + kind = newKind ) + } else if (isFromMe && existingRoom.kind != RoomKind.Ongoing) { + // Even if it's an older message, if it's from me, the room is ongoing + rooms[roomId] = existingRoom.copy(kind = RoomKind.Ongoing) } else { // Don't update the room list state for older messages return@update currentState