fix: issue in which UI callback listeners were leaking
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
20a066dabb
commit
4d1fffe42a
|
|
@ -14,7 +14,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateIntAsState
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
|
|
@ -65,6 +64,7 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -162,10 +162,7 @@ private fun pxAsDp(px: Int): Dp {
|
||||||
).dp
|
).dp
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class,
|
|
||||||
ExperimentalAnimationApi::class
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChannelScreen(
|
fun ChannelScreen(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
|
|
@ -173,25 +170,30 @@ fun ChannelScreen(
|
||||||
useDrawer: Boolean,
|
useDrawer: Boolean,
|
||||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// Setup
|
// <editor-fold desc="State and effects">
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.startListening(createUiCallbackListener = true)
|
viewModel.listenToWsEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load/switch channel
|
DisposableEffect(Unit) {
|
||||||
|
val job = scope.launch { viewModel.listenToUiCallbacks() }
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// </editor-fold>
|
||||||
|
// <editor-fold desc="Load/switch channel">
|
||||||
val channelPermissions by rememberChannelPermissions(channelId, viewModel.ensuredSelfMember)
|
val channelPermissions by rememberChannelPermissions(channelId, viewModel.ensuredSelfMember)
|
||||||
|
|
||||||
LaunchedEffect(channelId) {
|
LaunchedEffect(channelId) {
|
||||||
viewModel.switchChannel(channelId)
|
viewModel.switchChannel(channelId)
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
// Keyboard height
|
// <editor-fold desc="Keyboard height handling">
|
||||||
|
|
||||||
val imeTarget = WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current)
|
val imeTarget = WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current)
|
||||||
val navigationBarsInset = WindowInsets.navigationBars.getBottom(LocalDensity.current)
|
val navigationBarsInset = WindowInsets.navigationBars.getBottom(LocalDensity.current)
|
||||||
val imeCurrentInset = WindowInsets.ime.getBottom(LocalDensity.current)
|
val imeCurrentInset = WindowInsets.ime.getBottom(LocalDensity.current)
|
||||||
|
|
@ -211,9 +213,8 @@ fun ChannelScreen(
|
||||||
imeInTransition = false
|
imeInTransition = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
// Attachment handling
|
// <editor-fold desc="Attachment handling">
|
||||||
|
|
||||||
val processFileUri: (Uri, String?) -> Unit = remember {
|
val processFileUri: (Uri, String?) -> Unit = remember {
|
||||||
{ uri, pickerIdentifier ->
|
{ uri, pickerIdentifier ->
|
||||||
DocumentFile.fromSingleUri(context, uri)?.let { file ->
|
DocumentFile.fromSingleUri(context, uri)?.let { file ->
|
||||||
|
|
@ -278,9 +279,8 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
// UI elements
|
// <editor-fold desc="UI elements">
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
val isScrolledToBottom = remember(lazyListState) {
|
val isScrolledToBottom = remember(lazyListState) {
|
||||||
|
|
@ -328,9 +328,8 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
// Sheets
|
// <editor-fold desc="Sheets">
|
||||||
|
|
||||||
var channelInfoSheetShown by remember { mutableStateOf(false) }
|
var channelInfoSheetShown by remember { mutableStateOf(false) }
|
||||||
if (channelInfoSheetShown) {
|
if (channelInfoSheetShown) {
|
||||||
val channelInfoSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val channelInfoSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
@ -399,9 +398,8 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
// Begin UI composition
|
// <editor-fold desc="Begin UI composition">
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
contentWindowInsets = WindowInsets(
|
contentWindowInsets = WindowInsets(
|
||||||
left = 0,
|
left = 0,
|
||||||
|
|
@ -1033,4 +1031,5 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// </editor-fold>
|
||||||
}
|
}
|
||||||
|
|
@ -498,258 +498,254 @@ class ChannelScreenViewModel @Inject constructor(
|
||||||
ackChannel(channel?.id ?: return, messageId)
|
ackChannel(channel?.id ?: return, messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startListening(createUiCallbackListener: Boolean = true) {
|
suspend fun listenToWsEvents() {
|
||||||
viewModelScope.launch {
|
withContext(RevoltAPI.realtimeContext) {
|
||||||
withContext(RevoltAPI.realtimeContext) {
|
flow {
|
||||||
flow {
|
while (true) {
|
||||||
while (true) {
|
emit(RevoltAPI.wsFrameChannel.receive())
|
||||||
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<ChannelScreenItem.RegularMessage>()
|
|
||||||
.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<ChannelScreenItem.RegularMessage>()
|
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
}
|
}.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<ChannelScreenItem.RegularMessage>()
|
||||||
|
.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<ChannelScreenItem.RegularMessage>()
|
||||||
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue