feat(logger_history): conversation title

This commit is contained in:
rhunk 2024-03-27 19:08:42 +01:00
parent e67dc157e6
commit 73a03b80ae
4 changed files with 95 additions and 67 deletions

View File

@ -37,7 +37,8 @@ class ModDatabase(
SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf( SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf(
"friends" to listOf( "friends" to listOf(
"id INTEGER PRIMARY KEY AUTOINCREMENT", "id INTEGER PRIMARY KEY AUTOINCREMENT",
"userId VARCHAR UNIQUE", "userId CHAR(36) UNIQUE",
"dmConversationId VARCHAR(36)",
"displayName VARCHAR", "displayName VARCHAR",
"mutableUsername VARCHAR", "mutableUsername VARCHAR",
"bitmojiId VARCHAR", "bitmojiId VARCHAR",
@ -45,7 +46,7 @@ class ModDatabase(
), ),
"groups" to listOf( "groups" to listOf(
"id INTEGER PRIMARY KEY AUTOINCREMENT", "id INTEGER PRIMARY KEY AUTOINCREMENT",
"conversationId VARCHAR UNIQUE", "conversationId CHAR(36) UNIQUE",
"name VARCHAR", "name VARCHAR",
"participantsCount INTEGER" "participantsCount INTEGER"
), ),
@ -87,13 +88,7 @@ class ModDatabase(
return database.rawQuery("SELECT * FROM groups", null).use { cursor -> return database.rawQuery("SELECT * FROM groups", null).use { cursor ->
val groups = mutableListOf<MessagingGroupInfo>() val groups = mutableListOf<MessagingGroupInfo>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
groups.add( groups.add(MessagingGroupInfo.fromCursor(cursor))
MessagingGroupInfo(
conversationId = cursor.getStringOrNull("conversationId")!!,
name = cursor.getStringOrNull("name")!!,
participantsCount = cursor.getInteger("participantsCount")
)
)
} }
groups groups
} }
@ -104,22 +99,7 @@ class ModDatabase(
val friends = mutableListOf<MessagingFriendInfo>() val friends = mutableListOf<MessagingFriendInfo>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
runCatching { runCatching {
friends.add( friends.add(MessagingFriendInfo.fromCursor(cursor))
MessagingFriendInfo(
userId = cursor.getStringOrNull("userId")!!,
displayName = cursor.getStringOrNull("displayName"),
mutableUsername = cursor.getStringOrNull("mutableUsername")!!,
bitmojiId = cursor.getStringOrNull("bitmojiId"),
selfieId = cursor.getStringOrNull("selfieId"),
streaks = cursor.getLongOrNull("expirationTimestamp")?.let {
FriendStreaks(
notify = cursor.getInteger("notify") == 1,
expirationTimestamp = it,
length = cursor.getInteger("length")
)
}
)
)
}.onFailure { }.onFailure {
context.log.error("Failed to parse friend", it) context.log.error("Failed to parse friend", it)
} }
@ -147,9 +127,10 @@ class ModDatabase(
executeAsync { executeAsync {
try { try {
database.execSQL( database.execSQL(
"INSERT OR REPLACE INTO friends (userId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?)", "INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)",
arrayOf( arrayOf(
friend.userId, friend.userId,
friend.dmConversationId,
friend.displayName, friend.displayName,
friend.mutableUsername, friend.mutableUsername,
friend.bitmojiId, friend.bitmojiId,
@ -208,20 +189,14 @@ class ModDatabase(
fun getFriendInfo(userId: String): MessagingFriendInfo? { fun getFriendInfo(userId: String): MessagingFriendInfo? {
return database.rawQuery("SELECT * FROM friends LEFT OUTER JOIN streaks ON friends.userId = streaks.id WHERE userId = ?", arrayOf(userId)).use { cursor -> return database.rawQuery("SELECT * FROM friends LEFT OUTER JOIN streaks ON friends.userId = streaks.id WHERE userId = ?", arrayOf(userId)).use { cursor ->
if (!cursor.moveToFirst()) return@use null if (!cursor.moveToFirst()) return@use null
MessagingFriendInfo( MessagingFriendInfo.fromCursor(cursor)
userId = cursor.getStringOrNull("userId")!!, }
displayName = cursor.getStringOrNull("displayName"), }
mutableUsername = cursor.getStringOrNull("mutableUsername")!!,
bitmojiId = cursor.getStringOrNull("bitmojiId"), fun findFriend(conversationId: String): MessagingFriendInfo? {
selfieId = cursor.getStringOrNull("selfieId"), return database.rawQuery("SELECT * FROM friends WHERE dmConversationId = ?", arrayOf(conversationId)).use { cursor ->
streaks = cursor.getLongOrNull("expirationTimestamp")?.let { if (!cursor.moveToFirst()) return@use null
FriendStreaks( MessagingFriendInfo.fromCursor(cursor)
notify = cursor.getInteger("notify") == 1,
expirationTimestamp = it,
length = cursor.getInteger("length")
)
}
)
} }
} }
@ -243,11 +218,7 @@ class ModDatabase(
fun getGroupInfo(conversationId: String): MessagingGroupInfo? { fun getGroupInfo(conversationId: String): MessagingGroupInfo? {
return database.rawQuery("SELECT * FROM groups WHERE conversationId = ?", arrayOf(conversationId)).use { cursor -> return database.rawQuery("SELECT * FROM groups WHERE conversationId = ?", arrayOf(conversationId)).use { cursor ->
if (!cursor.moveToFirst()) return@use null if (!cursor.moveToFirst()) return@use null
MessagingGroupInfo( MessagingGroupInfo.fromCursor(cursor)
conversationId = cursor.getStringOrNull("conversationId")!!,
name = cursor.getStringOrNull("name")!!,
participantsCount = cursor.getInteger("participantsCount")
)
} }
} }

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
@ -36,6 +37,8 @@ import me.rhunk.snapenhance.core.features.impl.downloader.decoder.DecodedAttachm
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder
import me.rhunk.snapenhance.download.DownloadProcessor import me.rhunk.snapenhance.download.DownloadProcessor
import me.rhunk.snapenhance.ui.manager.Routes import me.rhunk.snapenhance.ui.manager.Routes
import java.nio.ByteBuffer
import java.util.UUID
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -45,12 +48,15 @@ class LoggerHistoryRoot : Routes.Route() {
private var stringFilter by mutableStateOf("") private var stringFilter by mutableStateOf("")
private var reverseOrder by mutableStateOf(true) private var reverseOrder by mutableStateOf(true)
private inline fun decodeMessage(message: LoggedMessage, result: (contentType: ContentType, messageReader: ProtoReader, attachments: List<DecodedAttachment>) -> Unit) { private inline fun decodeMessage(message: LoggedMessage, result: (senderId: String?, contentType: ContentType, messageReader: ProtoReader, attachments: List<DecodedAttachment>) -> Unit) {
runCatching { runCatching {
val messageObject = JsonParser.parseString(String(message.messageData, Charsets.UTF_8)).asJsonObject val messageObject = JsonParser.parseString(String(message.messageData, Charsets.UTF_8)).asJsonObject
val senderId = messageObject.getAsJsonObject("mSenderId")?.getAsJsonArray("mId")?.map { it.asByte }?.toByteArray()?.let {
ByteBuffer.wrap(it).run { UUID(long, long) }.toString()
}
val messageContent = messageObject.getAsJsonObject("mMessageContent") val messageContent = messageObject.getAsJsonObject("mMessageContent")
val messageReader = messageContent.getAsJsonArray("mContent").map { it.asByte }.toByteArray().let { ProtoReader(it) } val messageReader = messageContent.getAsJsonArray("mContent").map { it.asByte }.toByteArray().let { ProtoReader(it) }
result(ContentType.fromMessageContainer(messageReader) ?: ContentType.UNKNOWN, messageReader, MessageDecoder.decode(messageContent)) result(senderId, ContentType.fromMessageContainer(messageReader) ?: ContentType.UNKNOWN, messageReader, MessageDecoder.decode(messageContent))
}.onFailure { }.onFailure {
context.log.error("Failed to decode message", it) context.log.error("Failed to decode message", it)
} }
@ -119,23 +125,32 @@ class LoggerHistoryRoot : Routes.Route() {
LaunchedEffect(Unit, message) { LaunchedEffect(Unit, message) {
runCatching { runCatching {
decodeMessage(message) { contentType, messageReader, attachments -> decodeMessage(message) { senderId, contentType, messageReader, attachments ->
val senderUsername = senderId?.let { context.modDatabase.getFriendInfo(it)?.mutableUsername } ?: "unknown sender"
@Composable
fun ContentHeader() {
Text("$senderUsername (${contentType.toString().lowercase()})", modifier = Modifier.padding(end = 4.dp), fontWeight = FontWeight.ExtraLight)
}
if (contentType == ContentType.CHAT) { if (contentType == ContentType.CHAT) {
val content = messageReader.getString(2, 1) ?: "[empty chat message]" val content = messageReader.getString(2, 1) ?: "[empty chat message]"
contentView = { contentView = {
Text(content, modifier = Modifier Column {
.fillMaxWidth() Text(content, modifier = Modifier
.pointerInput(Unit) { .fillMaxWidth()
detectTapGestures(onLongPress = { .pointerInput(Unit) {
context.androidContext.copyToClipboard(content) detectTapGestures(onLongPress = {
context.androidContext.copyToClipboard(content)
})
}) })
}) ContentHeader()
}
} }
return@runCatching return@runCatching
} }
contentView = { contentView = {
Column column@{ Column column@{
Text("[$contentType]")
if (attachments.isEmpty()) return@column if (attachments.isEmpty()) return@column
FlowRow( FlowRow(
@ -164,6 +179,7 @@ class LoggerHistoryRoot : Routes.Route() {
} }
} }
} }
ContentHeader()
} }
} }
} }
@ -194,8 +210,15 @@ class LoggerHistoryRoot : Routes.Route() {
expanded = expanded, expanded = expanded,
onExpandedChange = { expanded = it }, onExpandedChange = { expanded = it },
) { ) {
fun formatConversationId(conversationId: String?): String? {
if (conversationId == null) return null
return context.modDatabase.getGroupInfo(conversationId)?.name?.let { "Group $it" } ?: context.modDatabase.findFriend(conversationId)?.let {
"Friend " + (it.displayName?.let { name -> "$name (${it.mutableUsername})" } ?: it.mutableUsername)
} ?: conversationId
}
OutlinedTextField( OutlinedTextField(
value = selectedConversation ?: "Select a conversation", value = remember(selectedConversation) { formatConversationId(selectedConversation) ?: "Select a conversation" },
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
modifier = Modifier modifier = Modifier
@ -213,12 +236,12 @@ class LoggerHistoryRoot : Routes.Route() {
} }
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
conversations.forEach { conversation -> conversations.forEach { conversationId ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
selectedConversation = conversation selectedConversation = conversationId
expanded = false expanded = false
}, text = { }, text = {
Text(conversation) Text(remember(conversationId) { formatConversationId(conversationId) ?: "Unknown conversation" })
}) })
} }
} }
@ -278,7 +301,7 @@ class LoggerHistoryRoot : Routes.Route() {
) { messageData -> ) { messageData ->
if (stringFilter.isEmpty()) return@fetchMessages true if (stringFilter.isEmpty()) return@fetchMessages true
var isMatch = false var isMatch = false
decodeMessage(messageData) { contentType, messageReader, _ -> decodeMessage(messageData) { _, contentType, messageReader, _ ->
if (contentType == ContentType.CHAT) { if (contentType == ContentType.CHAT) {
val content = messageReader.getString(2, 1) ?: return@decodeMessage val content = messageReader.getString(2, 1) ?: return@decodeMessage
isMatch = content.contains(stringFilter, ignoreCase = true) isMatch = content.contains(stringFilter, ignoreCase = true)
@ -312,6 +335,7 @@ class LoggerHistoryRoot : Routes.Route() {
value = searchValue, value = searchValue,
onValueChange = { keyword -> onValueChange = { keyword ->
searchValue = keyword searchValue = keyword
stringFilter = keyword
}, },
keyboardActions = KeyboardActions(onDone = { focusRequester.freeFocus() }), keyboardActions = KeyboardActions(onDone = { focusRequester.freeFocus() }),
modifier = Modifier modifier = Modifier
@ -329,11 +353,6 @@ class LoggerHistoryRoot : Routes.Route() {
cursorColor = MaterialTheme.colorScheme.primary cursorColor = MaterialTheme.colorScheme.primary
) )
) )
ElevatedButton(onClick = {
stringFilter = searchValue
}) {
Text("Search")
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()

View File

@ -1,8 +1,13 @@
package me.rhunk.snapenhance.common.data package me.rhunk.snapenhance.common.data
import android.database.Cursor
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import me.rhunk.snapenhance.common.config.FeatureNotice import me.rhunk.snapenhance.common.config.FeatureNotice
import me.rhunk.snapenhance.common.util.ktx.getIntOrNull
import me.rhunk.snapenhance.common.util.ktx.getInteger
import me.rhunk.snapenhance.common.util.ktx.getLongOrNull
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
@ -71,17 +76,48 @@ data class MessagingGroupInfo(
val conversationId: String, val conversationId: String,
val name: String, val name: String,
val participantsCount: Int val participantsCount: Int
): Parcelable ): Parcelable {
companion object {
fun fromCursor(cursor: Cursor): MessagingGroupInfo {
return MessagingGroupInfo(
conversationId = cursor.getStringOrNull("conversationId")!!,
name = cursor.getStringOrNull("name")!!,
participantsCount = cursor.getInteger("participantsCount")
)
}
}
}
@Parcelize @Parcelize
data class MessagingFriendInfo( data class MessagingFriendInfo(
val userId: String, val userId: String,
val dmConversationId: String?,
val displayName: String?, val displayName: String?,
val mutableUsername: String, val mutableUsername: String,
val bitmojiId: String?, val bitmojiId: String?,
val selfieId: String?, val selfieId: String?,
var streaks: FriendStreaks?, var streaks: FriendStreaks?,
): Parcelable ): Parcelable {
companion object {
fun fromCursor(cursor: Cursor): MessagingFriendInfo {
return MessagingFriendInfo(
userId = cursor.getStringOrNull("userId")!!,
dmConversationId = cursor.getStringOrNull("dmConversationId"),
displayName = cursor.getStringOrNull("displayName"),
mutableUsername = cursor.getStringOrNull("mutableUsername")!!,
bitmojiId = cursor.getStringOrNull("bitmojiId"),
selfieId = cursor.getStringOrNull("selfieId"),
streaks = cursor.getLongOrNull("expirationTimestamp")?.let {
FriendStreaks(
notify = cursor.getIntOrNull("notify") == 1,
expirationTimestamp = it,
length = cursor.getIntOrNull("length") ?: 0
)
}
)
}
}
}
class StoryData( class StoryData(
val url: String, val url: String,

View File

@ -261,6 +261,7 @@ class SnapEnhance {
val friends = feedEntries.filter { it.friendUserId != null }.map { val friends = feedEntries.filter { it.friendUserId != null }.map {
MessagingFriendInfo( MessagingFriendInfo(
it.friendUserId!!, it.friendUserId!!,
appContext.database.getConversationLinkFromUserId(it.friendUserId!!)?.clientConversationId,
it.friendDisplayName, it.friendDisplayName,
it.friendDisplayUsername!!.split("|")[1], it.friendDisplayUsername!!.split("|")[1],
it.bitmojiAvatarId, it.bitmojiAvatarId,
@ -279,6 +280,7 @@ class SnapEnhance {
return appContext.database.getFriendInfo(uuid)?.let { return appContext.database.getFriendInfo(uuid)?.let {
MessagingFriendInfo( MessagingFriendInfo(
userId = it.userId!!, userId = it.userId!!,
dmConversationId = appContext.database.getConversationLinkFromUserId(it.userId!!)?.clientConversationId,
displayName = it.displayName, displayName = it.displayName,
mutableUsername = it.mutableUsername!!, mutableUsername = it.mutableUsername!!,
bitmojiId = it.bitmojiAvatarId, bitmojiId = it.bitmojiAvatarId,