feat: send begin/stop typing state

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-03-16 02:31:21 +01:00
parent 5f0b7b8c2c
commit 331736119b
3 changed files with 87 additions and 11 deletions

View File

@ -29,6 +29,8 @@ import chat.revolt.api.realtime.frames.receivable.ServerUpdateFrame
import chat.revolt.api.realtime.frames.receivable.UserRelationshipFrame import chat.revolt.api.realtime.frames.receivable.UserRelationshipFrame
import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
import chat.revolt.api.realtime.frames.sendable.BeginTypingFrame
import chat.revolt.api.realtime.frames.sendable.EndTypingFrame
import chat.revolt.api.realtime.frames.sendable.PingFrame import chat.revolt.api.realtime.frames.sendable.PingFrame
import chat.revolt.api.routes.server.fetchMember import chat.revolt.api.routes.server.fetchMember
import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.Channel
@ -704,4 +706,28 @@ object RealtimeSocket {
private suspend fun pushReconnectEvent() { private suspend fun pushReconnectEvent() {
RevoltAPI.wsFrameChannel.send(RealtimeSocketFrames.Reconnected) RevoltAPI.wsFrameChannel.send(RealtimeSocketFrames.Reconnected)
} }
suspend fun beginTyping(channelId: String) {
if (disconnectionState != DisconnectionState.Connected) return
val beginTypingFrame = BeginTypingFrame("BeginTyping", channelId)
socket?.send(
RevoltJson.encodeToString(
BeginTypingFrame.serializer(),
beginTypingFrame
)
)
}
suspend fun endTyping(channelId: String) {
if (disconnectionState != DisconnectionState.Connected) return
val endTypingFrame = EndTypingFrame("EndTyping", channelId)
socket?.send(
RevoltJson.encodeToString(
EndTypingFrame.serializer(),
endTypingFrame
)
)
}
} }

View File

@ -487,10 +487,7 @@ fun ChannelScreen(
} else { } else {
NativeMessageField( NativeMessageField(
value = viewModel.pendingMessageContent, value = viewModel.pendingMessageContent,
onValueChange = { onValueChange = viewModel::updatePendingMessageContent,
viewModel.pendingMessageContent = it
// viewModel.textSelection = it.selection
},
onSendMessage = viewModel::sendPendingMessage, onSendMessage = viewModel::sendPendingMessage,
onAddAttachment = { onAddAttachment = {
val isTiramisu = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU val isTiramisu = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU

View File

@ -20,6 +20,7 @@ import chat.revolt.api.internals.Roles
import chat.revolt.api.internals.SpecialUsers import chat.revolt.api.internals.SpecialUsers
import chat.revolt.api.internals.ULID import chat.revolt.api.internals.ULID
import chat.revolt.api.internals.hasPermission import chat.revolt.api.internals.hasPermission
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.RealtimeSocketFrames
import chat.revolt.api.realtime.frames.receivable.ChannelDeleteFrame import chat.revolt.api.realtime.frames.receivable.ChannelDeleteFrame
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
@ -57,6 +58,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
const val MAX_MESSAGE_LENGTH = 2000 const val MAX_MESSAGE_LENGTH = 2000
@ -108,6 +110,57 @@ class ChannelScreenViewModel : ViewModel() {
pendingReplies.add(reply) pendingReplies.add(reply)
} }
private var lastSentBeginTyping: Instant? = null
private fun startTyping() {
if (editingMessage != null) return
if (lastSentBeginTyping != null) {
val diff = Clock.System.now() - lastSentBeginTyping!!
if (diff.inWholeSeconds < 1) return
}
viewModelScope.launch {
withContext(RevoltAPI.realtimeContext) {
activeChannelId?.let {
RealtimeSocket.beginTyping(it)
}
}
}
lastSentBeginTyping = Clock.System.now()
}
private var stopTypingJob: Job? = null
private fun queueStopTyping() {
stopTypingJob = viewModelScope.launch {
delay(5000)
stopTyping()
}
}
private fun stopTyping() {
if (editingMessage != null) return
viewModelScope.launch {
withContext(RevoltAPI.realtimeContext) {
activeChannelId?.let {
RealtimeSocket.endTyping(it)
}
}
}
}
fun updatePendingMessageContent(newContent: String) {
pendingMessageContent = newContent
if (newContent.isNotBlank()) {
startTyping()
stopTypingJob?.cancel()
queueStopTyping()
} else {
stopTyping()
}
}
fun toggleReplyMentionFor(reply: SendMessageReply) { fun toggleReplyMentionFor(reply: SendMessageReply) {
val index = pendingReplies.indexOf(reply) val index = pendingReplies.indexOf(reply)
val newReply = SendMessageReply( val newReply = SendMessageReply(
@ -229,7 +282,7 @@ class ChannelScreenViewModel : ViewModel() {
replies = pendingReplies replies = pendingReplies
) )
pendingMessageContent = "" updatePendingMessageContent("")
hasNoMoreMessages = false hasNoMoreMessages = false
isSendingMessage = false isSendingMessage = false
pendingUploadProgress = 0f pendingUploadProgress = 0f
@ -248,7 +301,7 @@ class ChannelScreenViewModel : ViewModel() {
newContent = pendingMessageContent.trimIndent() newContent = pendingMessageContent.trimIndent()
) )
pendingMessageContent = "" updatePendingMessageContent("")
isSendingMessage = false isSendingMessage = false
} }
@ -466,7 +519,7 @@ class ChannelScreenViewModel : ViewModel() {
val message = renderableMessages.find { msg -> val message = renderableMessages.find { msg ->
msg.id == it.messageId msg.id == it.messageId
} ?: return@onEach } ?: return@onEach
pendingMessageContent = message.content ?: "" updatePendingMessageContent(message.content ?: "")
textSelection = textSelection =
(message.content?.length ?: 0) to (message.content?.length ?: 0) (message.content?.length ?: 0) to (message.content?.length ?: 0)
} }
@ -512,7 +565,7 @@ class ChannelScreenViewModel : ViewModel() {
fun cancelEditingMessage() { fun cancelEditingMessage() {
editingMessage = null editingMessage = null
pendingMessageContent = "" updatePendingMessageContent("")
} }
fun putAtCursorPosition(content: String) { fun putAtCursorPosition(content: String) {
@ -521,7 +574,7 @@ class ChannelScreenViewModel : ViewModel() {
// if out of bounds, just append // if out of bounds, just append
if (currentSelection.first > currentContent.length) { if (currentSelection.first > currentContent.length) {
pendingMessageContent = currentContent + content updatePendingMessageContent(currentContent + content)
textSelection = textSelection =
currentSelection.first + content.length to currentSelection.first + content.length currentSelection.first + content.length to currentSelection.first + content.length
return return
@ -531,7 +584,7 @@ class ChannelScreenViewModel : ViewModel() {
content + content +
currentContent.substring(currentSelection.second) currentContent.substring(currentSelection.second)
pendingMessageContent = newContent updatePendingMessageContent(newContent)
textSelection = textSelection =
currentSelection.first + content.length to currentSelection.first + content.length currentSelection.first + content.length to currentSelection.first + content.length
} }