feat: send begin/stop typing state
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
5f0b7b8c2c
commit
331736119b
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue