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