feat: channel unreads and channel start indicator
This commit is contained in:
parent
45bfefbebf
commit
b0579d1436
|
|
@ -8,6 +8,7 @@ import chat.revolt.api.realtime.DisconnectionState
|
|||
import chat.revolt.api.realtime.RealtimeSocket
|
||||
import chat.revolt.api.routes.user.fetchSelf
|
||||
import chat.revolt.api.schemas.*
|
||||
import chat.revolt.api.unreads.Unreads
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.*
|
||||
|
|
@ -75,6 +76,8 @@ object RevoltAPI {
|
|||
val emojiCache = mutableStateMapOf<String, Emoji>()
|
||||
val messageCache = mutableStateMapOf<String, Message>()
|
||||
|
||||
val unreads = Unreads()
|
||||
|
||||
var selfId: String? = null
|
||||
|
||||
var sessionToken: String = ""
|
||||
|
|
@ -89,8 +92,8 @@ object RevoltAPI {
|
|||
suspend fun loginAs(token: String) {
|
||||
setSessionHeader(token)
|
||||
fetchSelf()
|
||||
|
||||
startSocketOps()
|
||||
unreads.sync()
|
||||
}
|
||||
|
||||
suspend fun connectWS() {
|
||||
|
|
@ -154,6 +157,8 @@ object RevoltAPI {
|
|||
emojiCache.clear()
|
||||
messageCache.clear()
|
||||
|
||||
unreads.clear()
|
||||
|
||||
socketThread?.interrupt()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ enum class DisconnectionState {
|
|||
object RealtimeSocket {
|
||||
var socket: WebSocketSession? = null
|
||||
|
||||
private var _disconnectionState = mutableStateOf(DisconnectionState.Disconnected)
|
||||
private var _disconnectionState = mutableStateOf(DisconnectionState.Reconnecting)
|
||||
val disconnectionState: DisconnectionState
|
||||
get() = _disconnectionState.value
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ object RealtimeSocket {
|
|||
Log.d("RealtimeSocket", "Already connected to websocket. Refusing to connect again.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
socket?.close(CloseReason(CloseReason.Codes.NORMAL, "Reconnecting to websocket."))
|
||||
|
||||
RevoltHttp.ws(REVOLT_WEBSOCKET) {
|
||||
|
|
@ -52,7 +52,15 @@ object RealtimeSocket {
|
|||
val authFrameString =
|
||||
RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame)
|
||||
|
||||
Log.d("RealtimeSocket", "Sending authorization frame: $authFrameString")
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Sending authorization frame: ${
|
||||
authFrameString.replace(
|
||||
token,
|
||||
"X".repeat(token.length)
|
||||
)
|
||||
}"
|
||||
)
|
||||
send(RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame))
|
||||
|
||||
incoming.consumeEach { frame ->
|
||||
|
|
@ -126,6 +134,12 @@ object RealtimeSocket {
|
|||
|
||||
RevoltAPI.messageCache[messageFrame.id!!] = messageFrame
|
||||
|
||||
// Update last message ID for channel - important for unreads
|
||||
messageFrame.channel?.let {
|
||||
RevoltAPI.channelCache[it] =
|
||||
RevoltAPI.channelCache[it]!!.copy(lastMessageID = messageFrame.id)
|
||||
}
|
||||
|
||||
channelCallbacks[messageFrame.channel]?.onMessage(messageFrame)
|
||||
}
|
||||
"ChannelStartTyping" -> {
|
||||
|
|
@ -168,6 +182,16 @@ object RealtimeSocket {
|
|||
RevoltAPI.channelCache[channelUpdateFrame.id] =
|
||||
existing.mergeWithPartial(channelUpdateFrame.data)
|
||||
}
|
||||
"ChannelAck" -> {
|
||||
val channelAckFrame =
|
||||
RevoltJson.decodeFromString(ChannelAckFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received channel ack frame for ${channelAckFrame.id} with new newest ${channelAckFrame.messageId}."
|
||||
)
|
||||
|
||||
RevoltAPI.unreads.processExternalAck(channelAckFrame.id, channelAckFrame.messageId)
|
||||
}
|
||||
"Authenticated" -> {
|
||||
// No effect
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,4 +89,10 @@ suspend fun sendMessage(
|
|||
.bodyAsText()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun ackChannel(channelId: String, messageId: String = ULID.makeNext()) {
|
||||
RevoltHttp.put("/channels/$channelId/ack/$messageId") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package chat.revolt.api.routes.server
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import io.ktor.client.request.*
|
||||
|
||||
suspend fun ackServer(serverId: String) {
|
||||
RevoltHttp.put("/servers/$serverId/ack") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package chat.revolt.api.routes.sync
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.schemas.ChannelUnreadResponse
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
|
||||
suspend fun syncUnreads(): List<ChannelUnreadResponse> {
|
||||
val response = RevoltHttp.get("/sync/unreads") {
|
||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
}
|
||||
.bodyAsText()
|
||||
|
||||
return RevoltJson.decodeFromString(
|
||||
ListSerializer(ChannelUnreadResponse.serializer()),
|
||||
response
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable
|
||||
data class MessagesInChannel(
|
||||
|
|
@ -43,6 +43,7 @@ data class Channel(
|
|||
val description: String? = null,
|
||||
val recipients: List<String>? = null,
|
||||
val icon: AutumnResource? = null,
|
||||
@SerialName("last_message_id")
|
||||
val lastMessageID: String? = null,
|
||||
val active: Boolean? = null,
|
||||
val permissions: Long? = null,
|
||||
|
|
@ -107,4 +108,26 @@ enum class ChannelType(val value: String) {
|
|||
return encoder.encodeString(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChannelUserChoice(
|
||||
val channel: String,
|
||||
val user: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelUnreadResponse(
|
||||
@SerialName("_id")
|
||||
val id: ChannelUserChoice,
|
||||
val last_id: String? = null,
|
||||
val mentions: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChannelUnread(
|
||||
@SerialName("_id")
|
||||
val id: String,
|
||||
val last_id: String? = null,
|
||||
val mentions: List<String>? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package chat.revolt.api.unreads
|
||||
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.internals.ULID
|
||||
import chat.revolt.api.routes.channel.ackChannel
|
||||
import chat.revolt.api.routes.server.ackServer
|
||||
import chat.revolt.api.routes.sync.syncUnreads
|
||||
import chat.revolt.api.schemas.ChannelUnread
|
||||
|
||||
class Unreads {
|
||||
private val hasLoaded = mutableStateOf(false)
|
||||
private val channels = mutableStateMapOf<String, ChannelUnread>()
|
||||
|
||||
suspend fun sync() {
|
||||
channels.clear()
|
||||
channels.putAll(syncUnreads().associate {
|
||||
it.id.channel to ChannelUnread(
|
||||
id = it.id.channel,
|
||||
last_id = it.last_id,
|
||||
mentions = it.mentions
|
||||
)
|
||||
})
|
||||
hasLoaded.value = true
|
||||
}
|
||||
|
||||
fun getForChannel(channelId: String): ChannelUnread? {
|
||||
if (!hasLoaded.value) return null
|
||||
return channels[channelId]
|
||||
}
|
||||
|
||||
fun hasUnread(channelId: String, lastMessageId: String): Boolean {
|
||||
if (!hasLoaded.value) return false
|
||||
return (channels[channelId]?.last_id?.compareTo(lastMessageId) ?: 0) < 0
|
||||
}
|
||||
|
||||
suspend fun markAsRead(channelId: String, messageId: String, sync: Boolean = true) {
|
||||
if (!hasLoaded.value) return
|
||||
channels[channelId]?.let {
|
||||
if (it.last_id == messageId) {
|
||||
channels.remove(channelId)
|
||||
} else {
|
||||
channels[channelId] = it.copy(last_id = messageId)
|
||||
}
|
||||
}
|
||||
if (sync) {
|
||||
ackChannel(channelId, messageId)
|
||||
}
|
||||
}
|
||||
|
||||
fun processExternalAck(channelId: String, messageId: String) {
|
||||
channels[channelId]?.let {
|
||||
if (it.last_id == messageId) {
|
||||
channels.remove(channelId)
|
||||
} else {
|
||||
channels[channelId] = it.copy(last_id = messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markServerAsRead(serverId: String, sync: Boolean = true) {
|
||||
if (!hasLoaded.value) return
|
||||
|
||||
val server = RevoltAPI.serverCache[serverId] ?: return
|
||||
server.channels?.forEach { channel ->
|
||||
channels[channel] = channels[channel]?.copy(last_id = ULID.makeNext()) ?: ChannelUnread(
|
||||
channel,
|
||||
ULID.makeNext()
|
||||
)
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
ackServer(serverId)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
channels.clear()
|
||||
hasLoaded.value = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,23 @@
|
|||
package chat.revolt.components.screens.chat.drawer.server
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.components.screens.chat.ChannelIcon
|
||||
|
|
@ -21,25 +27,58 @@ fun DrawerChannel(
|
|||
channelType: ChannelType,
|
||||
name: String,
|
||||
selected: Boolean,
|
||||
hasUnread: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val backgroundColor = animateColorAsState(
|
||||
if (selected) MaterialTheme.colorScheme.background
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
animationSpec = spring()
|
||||
)
|
||||
|
||||
val unreadDotOpacity = animateFloatAsState(
|
||||
if (hasUnread) 1f else 0f,
|
||||
animationSpec = spring()
|
||||
)
|
||||
val channelAlpha = animateFloatAsState(
|
||||
if (hasUnread) 1f else 0.8f,
|
||||
animationSpec = spring()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
if (selected) MaterialTheme.colorScheme.surface
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
|
||||
)
|
||||
.background(backgroundColor.value)
|
||||
.alpha(channelAlpha.value)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ChannelIcon(channelType = channelType, modifier = Modifier.padding(end = 8.dp))
|
||||
Text(
|
||||
text = name,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
if (hasUnread) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = (-8).dp)
|
||||
.clip(CircleShape)
|
||||
.background(LocalContentColor.current)
|
||||
.alpha(unreadDotOpacity.value)
|
||||
.size(8.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +201,12 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
|||
selected = channel.id == (navBackStackEntry?.arguments?.getString(
|
||||
"channelId"
|
||||
) ?: false),
|
||||
hasUnread = channel.lastMessageID?.let { lastMessageID ->
|
||||
RevoltAPI.unreads.hasUnread(
|
||||
channel.id!!,
|
||||
lastMessageID
|
||||
)
|
||||
} ?: false,
|
||||
onClick = {
|
||||
navController.navigate("channel/${channel.id}")
|
||||
scope.launch {
|
||||
|
|
@ -234,6 +240,12 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
|||
selected = navBackStackEntry?.arguments?.getString(
|
||||
"channelId"
|
||||
) == ch.id,
|
||||
hasUnread = ch.lastMessageID?.let { lastMessageID ->
|
||||
RevoltAPI.unreads.hasUnread(
|
||||
ch.id!!,
|
||||
lastMessageID
|
||||
)
|
||||
} ?: true,
|
||||
onClick = {
|
||||
scope.launch { drawerState.focusCenter() }
|
||||
navController.navigate("channel/${ch.id}") {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -29,38 +28,34 @@ fun ChannelInfoSheet(
|
|||
return
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ChannelIcon(
|
||||
channelType = channel.channelType ?: ChannelType.TextChannel,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
PageHeader(
|
||||
text = channel.name ?: channel.id ?: "",
|
||||
modifier = Modifier.offset((-4).dp, 0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.channel_info_sheet_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
ChannelIcon(
|
||||
channelType = channel.channelType ?: ChannelType.TextChannel,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
text = channel.description
|
||||
?: stringResource(id = R.string.channel_info_sheet_description_empty),
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
PageHeader(
|
||||
text = channel.name ?: channel.id ?: "",
|
||||
modifier = Modifier.offset((-4).dp, 0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.channel_info_sheet_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
)
|
||||
Text(
|
||||
text = channel.description
|
||||
?: stringResource(id = R.string.channel_info_sheet_description_empty),
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -39,6 +40,7 @@ import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
|||
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||
import chat.revolt.api.routes.channel.SendMessageReply
|
||||
import chat.revolt.api.routes.channel.ackChannel
|
||||
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
||||
import chat.revolt.api.routes.channel.sendMessage
|
||||
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||
|
|
@ -54,6 +56,8 @@ import chat.revolt.components.screens.chat.ChannelIcon
|
|||
import chat.revolt.components.screens.chat.ReplyManager
|
||||
import chat.revolt.components.screens.chat.TypingIndicator
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Instant
|
||||
|
|
@ -145,6 +149,14 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
_replies.clear()
|
||||
}
|
||||
|
||||
private var _noMoreMessages by mutableStateOf(false)
|
||||
val noMoreMessages: Boolean
|
||||
get() = _noMoreMessages
|
||||
|
||||
private fun setNoMoreMessages(noMore: Boolean) {
|
||||
_noMoreMessages = noMore
|
||||
}
|
||||
|
||||
private var _uiCallbackReceiver = mutableStateOf<UiCallbacks.CallbackReceiver?>(null)
|
||||
val uiCallbackReceiver: UiCallbacks.CallbackReceiver?
|
||||
get() = _uiCallbackReceiver.value
|
||||
|
|
@ -156,6 +168,7 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
regroupMessages(listOf(message) + renderableMessages)
|
||||
ackNewest()
|
||||
}
|
||||
|
||||
override fun onStartTyping(typing: ChannelStartTypingFrame) {
|
||||
|
|
@ -206,7 +219,11 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
viewModelScope.launch {
|
||||
val messages = arrayListOf<MessageSchema>()
|
||||
fetchMessagesFromChannel(channel!!.id!!, limit = 50, false).let {
|
||||
it.messages!!.forEach { message ->
|
||||
if (it.messages.isNullOrEmpty() || it.messages.size < 50) {
|
||||
setNoMoreMessages(true)
|
||||
}
|
||||
|
||||
it.messages?.forEach { message ->
|
||||
addUserIfUnknown(message.author ?: return@forEach)
|
||||
if (!RevoltAPI.messageCache.containsKey(message.id)) {
|
||||
RevoltAPI.messageCache[message.id!!] = message
|
||||
|
|
@ -233,7 +250,11 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
true,
|
||||
before = renderableMessages.last().id
|
||||
).let {
|
||||
it.messages!!.forEach { message ->
|
||||
if (it.messages.isNullOrEmpty() || it.messages.size < 50) {
|
||||
setNoMoreMessages(true)
|
||||
}
|
||||
|
||||
it.messages?.forEach { message ->
|
||||
addUserIfUnknown(message.author ?: return@forEach)
|
||||
if (!RevoltAPI.messageCache.containsKey(message.id)) {
|
||||
RevoltAPI.messageCache[message.id!!] = message
|
||||
|
|
@ -243,7 +264,11 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
}
|
||||
} else {
|
||||
fetchMessagesFromChannel(channel!!.id!!, limit = 50, true).let {
|
||||
it.messages!!.forEach { message ->
|
||||
if (it.messages.isNullOrEmpty() || it.messages.size < 50) {
|
||||
setNoMoreMessages(true)
|
||||
}
|
||||
|
||||
it.messages?.forEach { message ->
|
||||
addUserIfUnknown(message.author ?: return@forEach)
|
||||
if (!RevoltAPI.messageCache.containsKey(message.id)) {
|
||||
RevoltAPI.messageCache[message.id!!] = message
|
||||
|
|
@ -265,6 +290,10 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
registerCallbacks()
|
||||
|
||||
if (channel?.lastMessageID != null) {
|
||||
ackNewest()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPendingMessage() {
|
||||
|
|
@ -337,6 +366,27 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
|
||||
setRenderableMessages(groupedMessages)
|
||||
}
|
||||
|
||||
private var debouncedChannelAck: Job? = null
|
||||
private fun ackNewest() {
|
||||
if (debouncedChannelAck?.isActive == true) {
|
||||
debouncedChannelAck?.cancel()
|
||||
|
||||
Log.d("ChannelScreen", "Cancelling channel ack")
|
||||
}
|
||||
|
||||
if (channel?.lastMessageID == null) return
|
||||
|
||||
RevoltAPI.unreads.processExternalAck(channel!!.id!!, channel!!.lastMessageID!!)
|
||||
|
||||
debouncedChannelAck = viewModelScope.launch {
|
||||
delay(1000)
|
||||
if (channel?.lastMessageID == null) return@launch
|
||||
ackChannel(channel!!.id!!, channel!!.lastMessageID!!)
|
||||
|
||||
Log.d("ChannelScreen", "Acking channel")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -450,6 +500,7 @@ fun ChannelScreen(
|
|||
.collect {
|
||||
if (it) {
|
||||
coroutineScope.launch {
|
||||
if (viewModel.noMoreMessages) return@launch
|
||||
viewModel.fetchOlderMessages()
|
||||
}
|
||||
}
|
||||
|
|
@ -472,11 +523,25 @@ fun ChannelScreen(
|
|||
}
|
||||
|
||||
item {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
if (viewModel.noMoreMessages) {
|
||||
Text(
|
||||
text = stringResource(R.string.start_of_conversation),
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.padding(start = 8.dp, end = 8.dp, top = 64.dp, bottom = 32.dp)
|
||||
.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,8 @@
|
|||
<string name="channel_group">Group</string>
|
||||
<string name="channel_notes">Notes</string>
|
||||
|
||||
<string name="start_of_conversation">This is the start of your conversation</string>
|
||||
|
||||
<string name="message_field_placeholder_dm">Message @%1$s</string>
|
||||
<string name="message_field_placeholder_text">Message #%1$s</string>
|
||||
<string name="message_field_placeholder_voice">Message #%1$s</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue