feat: extremely rudimentary editing prone to breakage

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-06-12 20:04:51 +02:00
parent 21c2556eb1
commit 1bd77254ad
7 changed files with 104 additions and 12 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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))
}
} }

View File

@ -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

View File

@ -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,
) )
} }
} }

View File

@ -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 = ""
}
} }

View File

@ -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()
} }
} }