diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 3c7a671..58b016e 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -50,6 +51,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_send @@ -74,7 +76,10 @@ fun ChatScreen(id: Long) { val viewModel = LocalNostrViewModel.current // Get chat room by ID - val room = viewModel.getChatRoom(id) + val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() + val room by remember(id) { + derivedStateOf { chatRooms.firstOrNull { it.id == id } } + } // Show empty screen if (room == null) { @@ -91,8 +96,8 @@ fun ChatScreen(id: Long) { return } - val displayName by remember(room) { room.displayNameFlow(viewModel) }.collectAsState("Loading...") - val picture by remember(room) { room.pictureFlow(viewModel) }.collectAsState(null) + val displayName by remember(room) { room!!.displayNameFlow(viewModel) }.collectAsState("Loading...") + val picture by remember(room) { room!!.pictureFlow(viewModel) }.collectAsState(null) var text by remember { mutableStateOf("") } var loading by remember { mutableStateOf(true) } @@ -157,7 +162,7 @@ fun ChatScreen(id: Long) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { - room.members.firstOrNull()?.let { pubkey -> + room!!.members.firstOrNull()?.let { pubkey -> navigator.navigate(Screen.Profile(pubkey.toBech32())) } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index 3532afd..edee2a8 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -13,14 +13,10 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -63,6 +59,12 @@ class NostrViewModel( private val _isRelayListEmpty = MutableStateFlow(false) val isRelayListEmpty = _isRelayListEmpty.asStateFlow() + private val _chatRooms = MutableStateFlow>(emptySet()) + val chatRooms = _chatRooms.asStateFlow() + + private val _contactList = MutableStateFlow>(emptySet()) + val contactList = _contactList.asStateFlow() + private val _newEvents = MutableSharedFlow(extraBufferCapacity = 100) val newEvents = _newEvents.asSharedFlow() @@ -75,28 +77,6 @@ class NostrViewModel( private val _metadataStore = mutableMapOf>() private val metadataRequestChannel = Channel(Channel.UNLIMITED) private val seenPublicKeys = mutableSetOf() - private val manualRoomUpdates = MutableSharedFlow>() - private val _chatRooms = MutableStateFlow>(emptySet()) - - val chatRooms: StateFlow> = merge( - nostr.newEvents.map { event -> - processIncomingEvent(event) - _chatRooms.value - }, - manualRoomUpdates - ).stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = emptySet() - ) - - val contactList: StateFlow> = nostr.contactListUpdates - .map { it.toSet() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = emptySet() - ) init { // Skip the splash screen if a user is already logged in @@ -178,6 +158,33 @@ class NostrViewModel( } private suspend fun runObserver() = coroutineScope { + // Observe new messages + launch { + nostr.newEvents.collect { event -> + val roomId = event.roomId() + val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId } + + if (existingRoom == null) { + val currentUser = nostr.signer.currentUser + if (currentUser != null) { + val newRoom = Room.new(event, currentUser) + _chatRooms.update { (it + newRoom).sortedDescending().toSet() } + } + } else { + updateRoomList(roomId, event) + } + + _newEvents.emit(event) + } + } + + // Observe contact list updates + launch { + nostr.contactListUpdates.collect { contacts -> + _contactList.value = contacts.toSet() + } + } + // Observe metadata updates launch { nostr.metadataUpdates.collect { (pubkey, metadata) ->