diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 4c71506c..ac3ab302 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -14,7 +14,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.fadeIn @@ -65,6 +64,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -162,10 +162,7 @@ private fun pxAsDp(px: Int): Dp { ).dp } -@OptIn( - ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, - ExperimentalAnimationApi::class -) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun ChannelScreen( channelId: String, @@ -173,25 +170,30 @@ fun ChannelScreen( useDrawer: Boolean, viewModel: ChannelScreenViewModel = hiltViewModel() ) { - // Setup - + // val scope = rememberCoroutineScope() val context = LocalContext.current LaunchedEffect(Unit) { - viewModel.startListening(createUiCallbackListener = true) + viewModel.listenToWsEvents() } - // Load/switch channel + DisposableEffect(Unit) { + val job = scope.launch { viewModel.listenToUiCallbacks() } + onDispose { + job.cancel() + } + } + // + // val channelPermissions by rememberChannelPermissions(channelId, viewModel.ensuredSelfMember) LaunchedEffect(channelId) { viewModel.switchChannel(channelId) } - - // Keyboard height - + // + // val imeTarget = WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) val navigationBarsInset = WindowInsets.navigationBars.getBottom(LocalDensity.current) val imeCurrentInset = WindowInsets.ime.getBottom(LocalDensity.current) @@ -211,9 +213,8 @@ fun ChannelScreen( imeInTransition = false } } - - // Attachment handling - + // + // val processFileUri: (Uri, String?) -> Unit = remember { { uri, pickerIdentifier -> DocumentFile.fromSingleUri(context, uri)?.let { file -> @@ -278,9 +279,8 @@ fun ChannelScreen( } } } - - // UI elements - + // + // val lazyListState = rememberLazyListState() val isScrolledToBottom = remember(lazyListState) { @@ -328,9 +328,8 @@ fun ChannelScreen( } } } - - // Sheets - + // + // var channelInfoSheetShown by remember { mutableStateOf(false) } if (channelInfoSheetShown) { val channelInfoSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -399,9 +398,8 @@ fun ChannelScreen( } } } - - // Begin UI composition - + // + // Scaffold( contentWindowInsets = WindowInsets( left = 0, @@ -1033,4 +1031,5 @@ fun ChannelScreen( } } } + // } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt index b4025675..f2be0d30 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -498,258 +498,254 @@ class ChannelScreenViewModel @Inject constructor( ackChannel(channel?.id ?: return, messageId) } - suspend fun startListening(createUiCallbackListener: Boolean = true) { - viewModelScope.launch { - withContext(RevoltAPI.realtimeContext) { - flow { - while (true) { - emit(RevoltAPI.wsFrameChannel.receive()) - } - }.onEach { - when (it) { - is MessageFrame -> { - if (it.channel != channel?.id) return@onEach - // If we already have the message we are just catching up on the WebSocket connection. Skip - if (items.any { m -> (m is ChannelScreenItem.RegularMessage && m.message.id == it.id) || (m is ChannelScreenItem.SystemMessage && m.message.id == it.id) }) return@onEach - - it.author?.let { userId -> - if (RevoltAPI.userCache[userId] == null) { - RevoltAPI.userCache[userId] = fetchUser(userId) - } - } - channel?.server?.let { serverId -> - try { - it.author?.let { userId -> - fetchMember(serverId, userId) - } - } catch (e: Exception) { - Log.e("ChannelScreenViewModel", "Failed to fetch member", e) - } - } - - if (didInitialChannelFetch) { // this check is so that we don't end up with a message that arrives at the same time as the initial fetch in front of the loading indicator - val newItem = when { - it.system != null -> ChannelScreenItem.SystemMessage(it) - else -> ChannelScreenItem.RegularMessage(it) - } - updateItems(listOf(newItem) + items.filter { m -> - if (m is ChannelScreenItem.ProspectiveMessage) { - m.message.id != it.nonce - } else { - true - } - }) - } - - it.id?.let { mid -> ackMessage(mid) } - } - - is MessageDeleteFrame -> { - if (it.channel != channel?.id) return@onEach - - val newRenderableMessages = - items.filter { m -> - if (m is ChannelScreenItem.RegularMessage) { - m.message.id != it.id - } else { - true - } - } - - updateItems(newRenderableMessages) - } - - - is MessageUpdateFrame -> { - if (it.channel != channel?.id) return@onEach - - val messageFrame = - RevoltJson.decodeFromJsonElement(MessageFrame.serializer(), it.data) - - val currentMessage = items.find { m -> - m is ChannelScreenItem.RegularMessage && m.message.id == it.id - } - if (currentMessage == null) return@onEach - - if (messageFrame.author != null) { - addUserIfUnknown(messageFrame.author) - } - - updateItems( - items.map { m -> - if (m is ChannelScreenItem.RegularMessage && m.message.id == it.id) { - ChannelScreenItem.RegularMessage( - m.message.mergeWithPartial(messageFrame) - ) - } else { - m - } - } - ) - } - - is MessageAppendFrame -> { - if (it.channel != channel?.id) return@onEach - - val hasMessage = items.any { currentMsg -> - currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id - } - - if (!hasMessage) return@onEach - - updateItems( - items.map { currentMsg -> - if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { - RevoltAPI.messageCache[it.id]?.let { m -> - ChannelScreenItem.RegularMessage(m) - } ?: return@map currentMsg - } else { - currentMsg - } - } - ) - } - - is MessageReactFrame -> { - if (it.channel_id != channel?.id) return@onEach - - val hasMessage = items - .filterIsInstance() - .any { msg -> - msg.message.id == it.id - } - - if (!hasMessage) return@onEach - - updateItems( - items.map { currentMsg -> - if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { - RevoltAPI.messageCache[it.id]?.let { m -> - ChannelScreenItem.RegularMessage(m) - } ?: return@map currentMsg - } else { - currentMsg - } - } - ) - } - - is MessageUnreactFrame -> { - if (it.channel_id != channel?.id) return@onEach - - val hasMessage = items - .filterIsInstance() - .any { msg -> - msg.message.id == it.id - } - - if (!hasMessage) return@onEach - - updateItems( - items.map { currentMsg -> - if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { - RevoltAPI.messageCache[it.id]?.let { m -> - ChannelScreenItem.RegularMessage(m) - } ?: return@map currentMsg - } else { - currentMsg - } - } - ) - } - - is ChannelStartTypingFrame -> { - if (it.id != channel?.id) return@onEach - if (typingUsers.contains(it.user)) return@onEach - if (it.user == RevoltAPI.selfId) return@onEach - - addUserIfUnknown(it.user) - typingUsers.add(it.user) - } - - is ChannelStopTypingFrame -> { - if (it.id != channel?.id) return@onEach - if (!typingUsers.contains(it.user)) return@onEach - - typingUsers.remove(it.user) - } - - is ChannelDeleteFrame -> { - if (it.id != channel?.id) return@onEach - // FIXME This is UI logic from the view model. Too bad! - ActionChannel.send( - Action.ChatNavigate( - ChatRouterDestination.NoCurrentChannel( - channel?.server ?: return@onEach - ) - ) - ) - } - - is RealtimeSocketFrames.Reconnected -> { - Log.d("ChannelScreen", "Reconnected to WS.") - loadMessages(50, ignoreExisting = true) - startListening(createUiCallbackListener = false) - } - } - }.catch { - Log.e("ChannelScreen", "Failed to receive WS frame", it) - }.launchIn(this) - } - } - - if (createUiCallbackListener) { - viewModelScope.launch { - withContext(Dispatchers.Main) { - UiCallbacks.uiCallbackFlow.onEach { - Log.d("ChannelScreen", "Received UI callback: $it") - - when (it) { - is UiCallback.ReplyToMessage -> { - val message = items.find { m -> - m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId - } as? ChannelScreenItem.RegularMessage ?: return@onEach - - val shouldMention = kvStorage.getBoolean("mentionOnReply") ?: false - draftReplyTo.add( - SendMessageReply( - message.message.id ?: return@onEach, - shouldMention - ) - ) - } - - is UiCallback.EditMessage -> { - editingMessage = it.messageId - val message = items.find { m -> - m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId - } as? ChannelScreenItem.RegularMessage ?: return@onEach - - putDraftContent(message.message.content ?: "") - this@ChannelScreenViewModel.draftAttachments.clear() - draftReplyTo.clear() - } - - is UiCallback.ReplyToMessageWithContent -> { - val message = items.find { m -> - m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId - } as? ChannelScreenItem.RegularMessage ?: return@onEach - - val shouldMention = kvStorage.getBoolean("mentionOnReply") ?: false - draftReplyTo.add( - SendMessageReply( - message.message.id ?: return@onEach, - shouldMention - ) - ) - putDraftContent(it.content) - } - } - }.catch { - Log.e("ChannelScreen", "Failed to receive UI callback", it) - }.launchIn(this) + suspend fun listenToWsEvents() { + withContext(RevoltAPI.realtimeContext) { + flow { + while (true) { + emit(RevoltAPI.wsFrameChannel.receive()) } - } + }.onEach { + when (it) { + is MessageFrame -> { + if (it.channel != channel?.id) return@onEach + // If we already have the message we are just catching up on the WebSocket connection. Skip + if (items.any { m -> (m is ChannelScreenItem.RegularMessage && m.message.id == it.id) || (m is ChannelScreenItem.SystemMessage && m.message.id == it.id) }) return@onEach + + it.author?.let { userId -> + if (RevoltAPI.userCache[userId] == null) { + RevoltAPI.userCache[userId] = fetchUser(userId) + } + } + channel?.server?.let { serverId -> + try { + it.author?.let { userId -> + fetchMember(serverId, userId) + } + } catch (e: Exception) { + Log.e("ChannelScreenViewModel", "Failed to fetch member", e) + } + } + + if (didInitialChannelFetch) { // this check is so that we don't end up with a message that arrives at the same time as the initial fetch in front of the loading indicator + val newItem = when { + it.system != null -> ChannelScreenItem.SystemMessage(it) + else -> ChannelScreenItem.RegularMessage(it) + } + updateItems(listOf(newItem) + items.filter { m -> + if (m is ChannelScreenItem.ProspectiveMessage) { + m.message.id != it.nonce + } else { + true + } + }) + } + + it.id?.let { mid -> ackMessage(mid) } + } + + is MessageDeleteFrame -> { + if (it.channel != channel?.id) return@onEach + + val newRenderableMessages = + items.filter { m -> + if (m is ChannelScreenItem.RegularMessage) { + m.message.id != it.id + } else { + true + } + } + + updateItems(newRenderableMessages) + } + + + is MessageUpdateFrame -> { + if (it.channel != channel?.id) return@onEach + + val messageFrame = + RevoltJson.decodeFromJsonElement(MessageFrame.serializer(), it.data) + + val currentMessage = items.find { m -> + m is ChannelScreenItem.RegularMessage && m.message.id == it.id + } + if (currentMessage == null) return@onEach + + if (messageFrame.author != null) { + addUserIfUnknown(messageFrame.author) + } + + updateItems( + items.map { m -> + if (m is ChannelScreenItem.RegularMessage && m.message.id == it.id) { + ChannelScreenItem.RegularMessage( + m.message.mergeWithPartial(messageFrame) + ) + } else { + m + } + } + ) + } + + is MessageAppendFrame -> { + if (it.channel != channel?.id) return@onEach + + val hasMessage = items.any { currentMsg -> + currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id + } + + if (!hasMessage) return@onEach + + updateItems( + items.map { currentMsg -> + if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { + RevoltAPI.messageCache[it.id]?.let { m -> + ChannelScreenItem.RegularMessage(m) + } ?: return@map currentMsg + } else { + currentMsg + } + } + ) + } + + is MessageReactFrame -> { + if (it.channel_id != channel?.id) return@onEach + + val hasMessage = items + .filterIsInstance() + .any { msg -> + msg.message.id == it.id + } + + if (!hasMessage) return@onEach + + updateItems( + items.map { currentMsg -> + if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { + RevoltAPI.messageCache[it.id]?.let { m -> + ChannelScreenItem.RegularMessage(m) + } ?: return@map currentMsg + } else { + currentMsg + } + } + ) + } + + is MessageUnreactFrame -> { + if (it.channel_id != channel?.id) return@onEach + + val hasMessage = items + .filterIsInstance() + .any { msg -> + msg.message.id == it.id + } + + if (!hasMessage) return@onEach + + updateItems( + items.map { currentMsg -> + if (currentMsg is ChannelScreenItem.RegularMessage && currentMsg.message.id == it.id) { + RevoltAPI.messageCache[it.id]?.let { m -> + ChannelScreenItem.RegularMessage(m) + } ?: return@map currentMsg + } else { + currentMsg + } + } + ) + } + + is ChannelStartTypingFrame -> { + if (it.id != channel?.id) return@onEach + if (typingUsers.contains(it.user)) return@onEach + if (it.user == RevoltAPI.selfId) return@onEach + + addUserIfUnknown(it.user) + typingUsers.add(it.user) + } + + is ChannelStopTypingFrame -> { + if (it.id != channel?.id) return@onEach + if (!typingUsers.contains(it.user)) return@onEach + + typingUsers.remove(it.user) + } + + is ChannelDeleteFrame -> { + if (it.id != channel?.id) return@onEach + // FIXME This is UI logic from the view model. Too bad! + ActionChannel.send( + Action.ChatNavigate( + ChatRouterDestination.NoCurrentChannel( + channel?.server ?: return@onEach + ) + ) + ) + } + + is RealtimeSocketFrames.Reconnected -> { + Log.d("ChannelScreen", "Reconnected to WS.") + loadMessages(50, ignoreExisting = true) + listenToWsEvents() + } + } + }.catch { + Log.e("ChannelScreen", "Failed to receive WS frame", it) + }.launchIn(this) + } + } + + suspend fun listenToUiCallbacks() { + withContext(Dispatchers.Main) { + UiCallbacks.uiCallbackFlow.onEach { + Log.d("ChannelScreen", "Received UI callback: $it") + + when (it) { + is UiCallback.ReplyToMessage -> { + val message = items.find { m -> + m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId + } as? ChannelScreenItem.RegularMessage ?: return@onEach + + val shouldMention = kvStorage.getBoolean("mentionOnReply") ?: false + draftReplyTo.add( + SendMessageReply( + message.message.id ?: return@onEach, + shouldMention + ) + ) + } + + is UiCallback.EditMessage -> { + editingMessage = it.messageId + val message = items.find { m -> + m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId + } as? ChannelScreenItem.RegularMessage ?: return@onEach + + putDraftContent(message.message.content ?: "") + this@ChannelScreenViewModel.draftAttachments.clear() + draftReplyTo.clear() + } + + is UiCallback.ReplyToMessageWithContent -> { + val message = items.find { m -> + m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId + } as? ChannelScreenItem.RegularMessage ?: return@onEach + + val shouldMention = kvStorage.getBoolean("mentionOnReply") ?: false + draftReplyTo.add( + SendMessageReply( + message.message.id ?: return@onEach, + shouldMention + ) + ) + putDraftContent(it.content) + } + } + }.catch { + Log.e("ChannelScreen", "Failed to receive UI callback", it) + }.launchIn(this) } }