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.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
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
val RevoltJson = Json { ignoreUnknownKeys = true }
@OptIn(ExperimentalSerializationApi::class)
val RevoltJson = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
val RevoltHttp = HttpClient(OkHttp) {
install(DefaultRequest)

View File

@ -1,6 +1,7 @@
package chat.revolt.api.routes.channel
import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.api.internals.ULID
@ -9,12 +10,14 @@ import chat.revolt.api.schemas.Message
import chat.revolt.api.schemas.MessagesInChannel
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
suspend fun fetchMessagesFromChannel(
@ -72,6 +75,11 @@ data class SendMessageBody(
val attachments: List<String>?,
)
@kotlinx.serialization.Serializable
data class EditMessageBody(
val content: String?,
)
suspend fun sendMessage(
channelId: String,
content: String,
@ -97,6 +105,31 @@ suspend fun sendMessage(
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()) {
RevoltHttp.put("/channels/$channelId/ack/$messageId") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
sealed class UiCallback {
data class ReplyToMessage(val messageId: String) : UiCallback()
data class EditMessage(val messageId: String) : UiCallback()
}
object UiCallbacks {
@ -12,4 +13,8 @@ object UiCallbacks {
suspend fun replyToMessage(messageId: String) {
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.material.icons.Icons
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -57,6 +59,8 @@ fun MessageField(
modifier: Modifier = Modifier,
forceSendButton: Boolean = false,
disabled: Boolean = false,
editMode: Boolean = false,
cancelEdit: () -> Unit = {},
) {
val focusRequester = remember { FocusRequester() }
val placeholderResource = when (channelType) {
@ -73,6 +77,7 @@ fun MessageField(
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
) {
BasicTextField(
value = messageContent,
onValueChange = onMessageContentChange,
@ -118,15 +123,23 @@ fun MessageField(
contentPadding = PaddingValues(16.dp),
leadingIcon = {
Icon(
Icons.Default.Add,
when {
editMode -> Icons.Default.Close
else -> Icons.Default.Add
},
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
contentDescription = stringResource(id = R.string.add_attachment_alt),
modifier = Modifier
.clip(CircleShape)
.size(32.dp)
.clickable {
focusRequester.freeFocus() // hide keyboard because it's annoying
onAddAttachment()
when {
editMode -> cancelEdit()
else -> {
focusRequester.freeFocus() // hide keyboard because it's annoying
onAddAttachment()
}
}
}
.padding(4.dp)
.testTag("add_attachment")
@ -143,7 +156,10 @@ fun MessageField(
targetOffsetY = { it }
) + fadeOut(animationSpec = RevoltTweenFloat)) {
Icon(
Icons.Default.Send,
when {
editMode -> Icons.Default.Edit
else -> Icons.Default.Send
},
tint = MaterialTheme.colorScheme.primary,
contentDescription = stringResource(id = R.string.send_alt),
modifier = Modifier

View File

@ -404,7 +404,9 @@ fun ChannelScreen(
R.string.unknown
),
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.routes.channel.SendMessageReply
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.fetchSingleChannel
import chat.revolt.api.routes.channel.sendMessage
@ -58,6 +59,8 @@ class ChannelScreenViewModel : ViewModel() {
var pendingUploadProgress by mutableFloatStateOf(0f)
var editingMessage by mutableStateOf<String?>(null)
private fun popAttachmentBatch() {
pendingAttachments =
pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList()
@ -141,6 +144,11 @@ class ChannelScreenViewModel : ViewModel() {
}
fun sendPendingMessage() {
if (editingMessage != null) {
editPendingMessage()
return
}
isSendingMessage = true
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) {
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 {
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 {
UiCallbacks.editMessage(messageId)
onHideSheet()
}
}