feat(ui/chat_action_menu): show chat edit history

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk
2024-06-09 19:50:28 +02:00
parent 31ae06ed62
commit bcbf54b35a
8 changed files with 119 additions and 32 deletions

View File

@@ -143,7 +143,7 @@ class LoggerHistoryRoot : Routes.Route() {
})
val edits by rememberAsyncMutableState(defaultValue = emptyList()) {
loggerWrapper.getMessageEdits(selectedConversation!!, message.messageId)
loggerWrapper.getChatEdits(selectedConversation!!, message.messageId)
}
edits.forEach { messageEdit ->
val date = remember {
@@ -152,10 +152,10 @@ class LoggerHistoryRoot : Routes.Route() {
Text(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(onLongPress = {
context.androidContext.copyToClipboard(messageEdit.messageText)
context.androidContext.copyToClipboard(messageEdit.message)
})
}.fillMaxWidth().padding(start = 4.dp),
text = messageEdit.messageText + " (edited at $date)",
text = messageEdit.message + " (edited at $date)",
fontWeight = FontWeight.Light,
fontStyle = FontStyle.Italic,
fontSize = 12.sp

View File

@@ -0,0 +1,6 @@
package me.rhunk.snapenhance.bridge.logger;
parcelable LoggedChatEdit {
long timestamp;
String message;
}

View File

@@ -1,6 +1,7 @@
package me.rhunk.snapenhance.bridge.logger;
import me.rhunk.snapenhance.bridge.logger.BridgeLoggedMessage;
import me.rhunk.snapenhance.bridge.logger.LoggedChatEdit;
interface LoggerInterface {
/**
@@ -38,4 +39,6 @@ interface LoggerInterface {
String eventType,
String data
);
List<LoggedChatEdit> getChatEdits(String conversationId, long messageId);
}

View File

@@ -1407,6 +1407,7 @@
"preview_button": "Preview",
"download_button": "Download",
"delete_logged_message_button": "Delete Logged Message",
"show_chat_edit_history": "Show Chat Edit History",
"convert_message": "Convert Message",
"edit_message": "Edit Message"
},

View File

@@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import kotlinx.coroutines.*
import me.rhunk.snapenhance.bridge.logger.BridgeLoggedMessage
import me.rhunk.snapenhance.bridge.logger.LoggedChatEdit
import me.rhunk.snapenhance.bridge.logger.LoggerInterface
import me.rhunk.snapenhance.common.bridge.InternalFileHandleType
import me.rhunk.snapenhance.common.data.StoryData
@@ -20,11 +21,6 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import java.io.File
import java.util.UUID
class LoggedMessageEdit(
val timestamp: Long,
val messageText: String
)
class LoggedMessage(
val messageId: Long,
val conversationId: String,
@@ -422,17 +418,17 @@ class LoggerWrapper(
return ConversationInfo(conversationId, participantSize, groupTitle, usernames)
}
fun getMessageEdits(conversationId: String, messageId: Long): List<LoggedMessageEdit> {
val edits = mutableListOf<LoggedMessageEdit>()
override fun getChatEdits(conversationId: String, messageId: Long): List<LoggedChatEdit> {
val edits = mutableListOf<LoggedChatEdit>()
database.rawQuery(
"SELECT added_timestamp, message_text FROM chat_edits WHERE conversation_id = ? AND message_id = ?",
"SELECT added_timestamp, message_text FROM chat_edits WHERE conversation_id = ? AND message_id = ? ORDER BY added_timestamp ASC",
arrayOf(conversationId, messageId.toString())
).use {
while (it.moveToNext()) {
edits.add(LoggedMessageEdit(
timestamp = it.getLongOrNull("added_timestamp") ?: continue,
messageText = it.getStringOrNull("message_text") ?: continue
))
).use { cursor ->
while (cursor.moveToNext()) {
edits.add(LoggedChatEdit().apply {
timestamp = cursor.getLongOrNull("added_timestamp") ?: return@apply
message = cursor.getStringOrNull("message_text")
}.takeIf { it.timestamp > 0L } ?: continue)
}
}
return edits

View File

@@ -8,6 +8,7 @@ import android.os.DeadObjectException
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.bridge.logger.BridgeLoggedMessage
import me.rhunk.snapenhance.bridge.logger.LoggedChatEdit
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MessageState
import me.rhunk.snapenhance.common.data.QuotedMessageContentStatus
@@ -76,6 +77,11 @@ class MessageLogger : Feature("MessageLogger",
}
}
fun getChatEdits(conversationId: String, clientMessageId: Long): List<LoggedChatEdit> {
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return emptyList()
return loggerInterface.getChatEdits(conversationId, uniqueMessageId)
}
private fun computeMessageIdentifier(conversationId: String, orderKey: Long) = (orderKey.toString() + conversationId).longHashCode()
private fun makeUniqueIdentifier(conversationId: String, clientMessageId: Long): Long? {

View File

@@ -5,6 +5,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button
import android.widget.LinearLayout
import me.rhunk.snapenhance.bridge.logger.LoggedChatEdit
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
@@ -131,6 +132,30 @@ class ChatActionMenu : AbstractMenu() {
}
}
})
injectButton(Button(viewGroup.context).apply {
var chatEdits = emptyList<LoggedChatEdit>()
text = this@ChatActionMenu.context.translation["chat_action_menu.show_chat_edit_history"]
setOnClickListener {
menuViewInjector.menu(NewChatActionMenu::class)?.showChatEditHistory(chatEdits)
}
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
visibility = View.GONE
chatEdits = this@ChatActionMenu.context.feature(MessageLogger::class).getChatEdits(
messaging.openedConversationUUID.toString(),
messaging.lastFocusedMessageId,
)
if (chatEdits.isEmpty()) return
visibility = View.VISIBLE
}
override fun onViewDetachedFromWindow(v: View) {
visibility = View.GONE
chatEdits = emptyList()
}
})
})
}
if (context.config.experimental.editMessage.get() && messaging.conversationManager?.isEditMessageSupported() == true) {

View File

@@ -10,29 +10,32 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.RemoveRedEye
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.rounded.BookmarkRemove
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.bridge.logger.LoggedChatEdit
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
import me.rhunk.snapenhance.common.util.ktx.copyToClipboard
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
@@ -48,9 +51,11 @@ import me.rhunk.snapenhance.core.ui.debugEditText
import me.rhunk.snapenhance.core.ui.iterateParent
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
import me.rhunk.snapenhance.core.util.ktx.isDarkTheme
import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.io.encoding.Base64
@@ -70,6 +75,32 @@ class NewChatActionMenu : AbstractMenu() {
}
}
fun showChatEditHistory(
edits: List<LoggedChatEdit>,
) {
createComposeAlertDialog(context.mainActivity!!) {
LazyColumn(
modifier = Modifier.padding(16.dp),
) {
itemsIndexed(edits) { index, edit ->
Column(
modifier = Modifier.padding(8.dp).fillMaxWidth().pointerInput(Unit) {
detectTapGestures(
onLongPress = {
context.androidContext.copyToClipboard(edit.message)
}
)
},
horizontalAlignment = Alignment.Start,
) {
Text(edit.message)
Text(text = DateFormat.getDateTimeInstance().format(edit.timestamp) + " (${index + 1})", fontSize = 12.sp, fontWeight = FontWeight.Light)
}
}
}
}.show()
}
fun editCurrentMessage(
localContext: Context,
dismissActionMenu: () -> Unit,
@@ -267,6 +298,11 @@ class NewChatActionMenu : AbstractMenu() {
val composeView = createComposeView(event.view.context) {
val primaryColor = remember { if (event.view.context.isDarkTheme()) Color.White else Color.Black }
val avenirNextMediumFont = remember {
FontFamily(
Font(context.resources.getIdentifier("avenir_next_medium", "font"), FontWeight.Medium)
)
}
@Composable
fun ListButton(
@@ -288,7 +324,7 @@ class NewChatActionMenu : AbstractMenu() {
tint = primaryColor,
contentDescription = text
)
Text(text, color = primaryColor)
Text(text, color = primaryColor, fontFamily = avenirNextMediumFont, fontSize = 16.sp)
}
Spacer(modifier = Modifier
.height(1.dp)
@@ -300,11 +336,11 @@ class NewChatActionMenu : AbstractMenu() {
modifier = Modifier.fillMaxWidth(),
) {
if (context.config.downloader.downloadContextMenu.get()) {
ListButton(icon = Icons.Default.RemoveRedEye, text = context.translation["chat_action_menu.preview_button"], modifier = Modifier.clickable {
ListButton(icon = Icons.Outlined.RemoveRedEye, text = context.translation["chat_action_menu.preview_button"], modifier = Modifier.clickable {
closeActionMenu()
mediaDownloader.onMessageActionMenu(true)
})
ListButton(icon = Icons.Default.Download, text = context.translation["chat_action_menu.download_button"], modifier = Modifier.pointerInput(Unit) {
ListButton(icon = Icons.Outlined.Download, text = context.translation["chat_action_menu.download_button"], modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
closeActionMenu()
@@ -319,7 +355,7 @@ class NewChatActionMenu : AbstractMenu() {
}
if (context.config.experimental.editMessage.get() && messaging.conversationManager?.isEditMessageSupported() == true) {
ListButton(icon = Icons.Rounded.Edit, text = context.translation["chat_action_menu.edit_message"], modifier = Modifier.clickable {
ListButton(icon = Icons.Outlined.Edit, text = context.translation["chat_action_menu.edit_message"], modifier = Modifier.clickable {
editCurrentMessage(event.view.context) {
context.runOnUiThread {
closeActionMenu()
@@ -329,7 +365,21 @@ class NewChatActionMenu : AbstractMenu() {
}
if (context.config.messaging.messageLogger.globalState == true) {
ListButton(icon = Icons.Rounded.BookmarkRemove, text = context.translation["chat_action_menu.delete_logged_message_button"], modifier = Modifier.clickable {
val chatEdits by rememberAsyncMutableState(defaultValue = null) {
context.feature(MessageLogger::class).getChatEdits(
messaging.openedConversationUUID.toString(),
messaging.lastFocusedMessageId
)
}
if (chatEdits != null && chatEdits?.isNotEmpty() == true) {
ListButton(icon = Icons.Outlined.History, text = context.translation["chat_action_menu.show_chat_edit_history"], modifier = Modifier.clickable {
closeActionMenu()
showChatEditHistory(chatEdits!!)
})
}
ListButton(icon = Icons.Outlined.BookmarkRemove, text = context.translation["chat_action_menu.delete_logged_message_button"], modifier = Modifier.clickable {
closeActionMenu()
context.executeAsync {
messageLogger.deleteMessage(messaging.openedConversationUUID.toString(), messaging.lastFocusedMessageId)