diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt index bb1ab89af..d9028faeb 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt @@ -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 diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggedChatEdit.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggedChatEdit.aidl new file mode 100644 index 000000000..9abb5d509 --- /dev/null +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggedChatEdit.aidl @@ -0,0 +1,6 @@ +package me.rhunk.snapenhance.bridge.logger; + +parcelable LoggedChatEdit { + long timestamp; + String message; +} \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl index cd7029088..f80570c11 100644 --- a/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl @@ -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 getChatEdits(String conversationId, long messageId); } \ No newline at end of file diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index b711b7510..13cea17a8 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -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" }, diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt index 18e97295f..2deec2986 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt @@ -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 { - val edits = mutableListOf() + override fun getChatEdits(conversationId: String, messageId: Long): List { + val edits = mutableListOf() 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 diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt index 00a2d6c93..ef786db48 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt @@ -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 { + 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? { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt index 06eebfe3c..b1c71078b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt @@ -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() + 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) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt index 9194905cb..66f46216f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt @@ -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, + ) { + 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)