feat: extremely rudimentary editing prone to breakage
Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
parent
21c2556eb1
commit
1bd77254ad
|
|
@ -33,6 +33,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.newSingleThreadContext
|
import kotlinx.coroutines.newSingleThreadContext
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import chat.revolt.api.schemas.Channel as ChannelSchema
|
import chat.revolt.api.schemas.Channel as ChannelSchema
|
||||||
|
|
@ -51,7 +52,11 @@ fun buildUserAgent(accessMethod: String = "Ktor"): String {
|
||||||
|
|
||||||
private const val BACKEND_IS_STABLE = false
|
private const val BACKEND_IS_STABLE = false
|
||||||
|
|
||||||
val RevoltJson = Json { ignoreUnknownKeys = true }
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val RevoltJson = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
|
||||||
val RevoltHttp = HttpClient(OkHttp) {
|
val RevoltHttp = HttpClient(OkHttp) {
|
||||||
install(DefaultRequest)
|
install(DefaultRequest)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package chat.revolt.api.routes.channel
|
package chat.revolt.api.routes.channel
|
||||||
|
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.RevoltError
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
import chat.revolt.api.RevoltJson
|
import chat.revolt.api.RevoltJson
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
|
|
@ -9,12 +10,14 @@ import chat.revolt.api.schemas.Message
|
||||||
import chat.revolt.api.schemas.MessagesInChannel
|
import chat.revolt.api.schemas.MessagesInChannel
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.parameter
|
import io.ktor.client.request.parameter
|
||||||
|
import io.ktor.client.request.patch
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.put
|
import io.ktor.client.request.put
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
|
||||||
suspend fun fetchMessagesFromChannel(
|
suspend fun fetchMessagesFromChannel(
|
||||||
|
|
@ -72,6 +75,11 @@ data class SendMessageBody(
|
||||||
val attachments: List<String>?,
|
val attachments: List<String>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class EditMessageBody(
|
||||||
|
val content: String?,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun sendMessage(
|
suspend fun sendMessage(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
content: String,
|
content: String,
|
||||||
|
|
@ -97,6 +105,31 @@ suspend fun sendMessage(
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun editMessage(
|
||||||
|
channelId: String,
|
||||||
|
messageId: String,
|
||||||
|
newContent: String? = null,
|
||||||
|
) {
|
||||||
|
val response = RevoltHttp.patch("/channels/$channelId/messages/$messageId") {
|
||||||
|
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||||
|
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
EditMessageBody(
|
||||||
|
content = newContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.bodyAsText()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||||
|
throw Error(error.type)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Not an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun ackChannel(channelId: String, messageId: String = ULID.makeNext()) {
|
suspend fun ackChannel(channelId: String, messageId: String = ULID.makeNext()) {
|
||||||
RevoltHttp.put("/channels/$channelId/ack/$messageId") {
|
RevoltHttp.put("/channels/$channelId/ack/$messageId") {
|
||||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
sealed class UiCallback {
|
sealed class UiCallback {
|
||||||
data class ReplyToMessage(val messageId: String) : UiCallback()
|
data class ReplyToMessage(val messageId: String) : UiCallback()
|
||||||
|
data class EditMessage(val messageId: String) : UiCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
object UiCallbacks {
|
object UiCallbacks {
|
||||||
|
|
@ -12,4 +13,8 @@ object UiCallbacks {
|
||||||
suspend fun replyToMessage(messageId: String) {
|
suspend fun replyToMessage(messageId: String) {
|
||||||
uiCallbackFlow.emit(UiCallback.ReplyToMessage(messageId))
|
uiCallbackFlow.emit(UiCallback.ReplyToMessage(messageId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun editMessage(messageId: String) {
|
||||||
|
uiCallbackFlow.emit(UiCallback.EditMessage(messageId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -57,6 +59,8 @@ fun MessageField(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceSendButton: Boolean = false,
|
forceSendButton: Boolean = false,
|
||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
|
editMode: Boolean = false,
|
||||||
|
cancelEdit: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val placeholderResource = when (channelType) {
|
val placeholderResource = when (channelType) {
|
||||||
|
|
@ -73,6 +77,7 @@ fun MessageField(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||||
) {
|
) {
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = messageContent,
|
value = messageContent,
|
||||||
onValueChange = onMessageContentChange,
|
onValueChange = onMessageContentChange,
|
||||||
|
|
@ -118,15 +123,23 @@ fun MessageField(
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Add,
|
when {
|
||||||
|
editMode -> Icons.Default.Close
|
||||||
|
else -> Icons.Default.Add
|
||||||
|
},
|
||||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
focusRequester.freeFocus() // hide keyboard because it's annoying
|
when {
|
||||||
onAddAttachment()
|
editMode -> cancelEdit()
|
||||||
|
else -> {
|
||||||
|
focusRequester.freeFocus() // hide keyboard because it's annoying
|
||||||
|
onAddAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.testTag("add_attachment")
|
.testTag("add_attachment")
|
||||||
|
|
@ -143,7 +156,10 @@ fun MessageField(
|
||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
) + fadeOut(animationSpec = RevoltTweenFloat)) {
|
) + fadeOut(animationSpec = RevoltTweenFloat)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Send,
|
when {
|
||||||
|
editMode -> Icons.Default.Edit
|
||||||
|
else -> Icons.Default.Send
|
||||||
|
},
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
contentDescription = stringResource(id = R.string.send_alt),
|
contentDescription = stringResource(id = R.string.send_alt),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,9 @@ fun ChannelScreen(
|
||||||
R.string.unknown
|
R.string.unknown
|
||||||
),
|
),
|
||||||
forceSendButton = viewModel.pendingAttachments.isNotEmpty(),
|
forceSendButton = viewModel.pendingAttachments.isNotEmpty(),
|
||||||
disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage
|
disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage,
|
||||||
|
editMode = viewModel.editingMessage != null,
|
||||||
|
cancelEdit = viewModel::cancelEditingMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
|
||||||
import chat.revolt.api.routes.channel.SendMessageReply
|
import chat.revolt.api.routes.channel.SendMessageReply
|
||||||
import chat.revolt.api.routes.channel.ackChannel
|
import chat.revolt.api.routes.channel.ackChannel
|
||||||
|
import chat.revolt.api.routes.channel.editMessage
|
||||||
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
||||||
import chat.revolt.api.routes.channel.fetchSingleChannel
|
import chat.revolt.api.routes.channel.fetchSingleChannel
|
||||||
import chat.revolt.api.routes.channel.sendMessage
|
import chat.revolt.api.routes.channel.sendMessage
|
||||||
|
|
@ -58,6 +59,8 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
|
|
||||||
var pendingUploadProgress by mutableFloatStateOf(0f)
|
var pendingUploadProgress by mutableFloatStateOf(0f)
|
||||||
|
|
||||||
|
var editingMessage by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
private fun popAttachmentBatch() {
|
private fun popAttachmentBatch() {
|
||||||
pendingAttachments =
|
pendingAttachments =
|
||||||
pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList()
|
pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList()
|
||||||
|
|
@ -141,6 +144,11 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPendingMessage() {
|
fun sendPendingMessage() {
|
||||||
|
if (editingMessage != null) {
|
||||||
|
editPendingMessage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isSendingMessage = true
|
isSendingMessage = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -184,6 +192,21 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun editPendingMessage() {
|
||||||
|
isSendingMessage = true
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
editMessage(
|
||||||
|
channelId = activeChannel!!.id!!,
|
||||||
|
messageId = editingMessage!!,
|
||||||
|
newContent = pendingMessageContent.trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
pendingMessageContent = ""
|
||||||
|
isSendingMessage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun regroupMessages(newMessages: List<Message> = renderableMessages) {
|
private suspend fun regroupMessages(newMessages: List<Message> = renderableMessages) {
|
||||||
val groupedMessages = mutableMapOf<String, Message>()
|
val groupedMessages = mutableMapOf<String, Message>()
|
||||||
|
|
||||||
|
|
@ -308,6 +331,14 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is UiCallback.EditMessage -> {
|
||||||
|
editingMessage = it.messageId
|
||||||
|
val message = renderableMessages.find { msg ->
|
||||||
|
msg.id == it.messageId
|
||||||
|
} ?: return@onEach
|
||||||
|
pendingMessageContent = message.content ?: ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.catch {
|
}.catch {
|
||||||
Log.e("ChannelScreen", "Failed to receive UI callback", it)
|
Log.e("ChannelScreen", "Failed to receive UI callback", it)
|
||||||
|
|
@ -347,4 +378,9 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelEditingMessage() {
|
||||||
|
editingMessage = null
|
||||||
|
pendingMessageContent = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -285,13 +285,8 @@ fun MessageContextSheet(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.comingsoon_toast),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
UiCallbacks.editMessage(messageId)
|
||||||
onHideSheet()
|
onHideSheet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue