commit c533983fb3ee67d4a744ca7e85c80287ce87bf28
parent a38e96906dfdedaa334f2e0e0a0278a454797e64
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 15 Oct 2023 00:30:31 +0200

feat(manager/social): chat purge

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 19++++++++++++++-----
Mcommon/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/MessagingBridge.aidl | 4+++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt | 3++-
4 files changed, 200 insertions(+), 16 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt @@ -1,21 +1,21 @@ package me.rhunk.snapenhance.ui.manager.sections.social +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.* import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge import me.rhunk.snapenhance.bridge.snapclient.types.Message @@ -23,22 +23,177 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.SocialScope import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper +import me.rhunk.snapenhance.ui.util.AlertDialogs +import me.rhunk.snapenhance.ui.util.Dialog class MessagingPreview( private val context: RemoteSideContext, private val scope: SocialScope, private val scopeId: String ) { + private val alertDialogs by lazy { AlertDialogs(context.translation) } + private lateinit var coroutineScope: CoroutineScope private lateinit var messagingBridge: MessagingBridge private lateinit var previewScrollState: LazyListState + private val myUserId by lazy { messagingBridge.myUserId } + private var conversationId: String? = null private val messages = sortedMapOf<Long, Message>() private var messageSize by mutableIntStateOf(0) private var lastMessageId = Long.MAX_VALUE + private val selectedMessages = mutableStateListOf<Long>() + + private fun toggleSelectedMessage(messageId: Long) { + if (selectedMessages.contains(messageId)) selectedMessages.remove(messageId) + else selectedMessages.add(messageId) + } + + + @Composable + fun TopBarAction() { + var deletedMessageCount by remember { mutableIntStateOf(0) } + var messageDeleteJob by remember { mutableStateOf(null as Job?) } + + fun deleteIndividualMessage(serverMessageId: Long) { + val message = messages[serverMessageId] ?: return + if (message.senderId != myUserId) return + + val error = messagingBridge.updateMessage(conversationId, message.clientMessageId, "ERASE") + + if (error != null) { + context.shortToast("Failed to delete message: $error") + } else { + coroutineScope.launch { + deletedMessageCount++ + messages.remove(serverMessageId) + messageSize = messages.size + } + } + } + + if (messageDeleteJob != null) { + Dialog(onDismissRequest = { + messageDeleteJob?.cancel() + messageDeleteJob = null + }) { + Card { + Column( + modifier = Modifier + .padding(20.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Deleting messages ($deletedMessageCount)") + Spacer(modifier = Modifier.height(10.dp)) + CircularProgressIndicator( + modifier = Modifier + .padding() + .size(30.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } + + + if (selectedMessages.isNotEmpty()) { + IconButton(onClick = { + deletedMessageCount = 0 + messageDeleteJob = coroutineScope.launch(Dispatchers.IO) { + selectedMessages.toList().also { + selectedMessages.clear() + }.forEach { messageId -> + deleteIndividualMessage(messageId) + } + }.apply { + invokeOnCompletion { + context.shortToast("Successfully deleted $deletedMessageCount messages") + messageDeleteJob = null + } + } + }) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = "Delete" + ) + } + + IconButton(onClick = { + selectedMessages.clear() + }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Close" + ) + } + } else { + var deleteAllConfirmationDialog by remember { mutableStateOf(false) } + + if (deleteAllConfirmationDialog) { + Dialog(onDismissRequest = { deleteAllConfirmationDialog = false }) { + alertDialogs.ConfirmDialog(title = "Are you sure you want to delete all your messages?", onDismiss = { + deleteAllConfirmationDialog = false + }, onConfirm = { + deletedMessageCount = 0 + deleteAllConfirmationDialog = false + messageDeleteJob = coroutineScope.launch(Dispatchers.IO) { + var lastMessageId = Long.MAX_VALUE + + do { + val fetchedMessages = messagingBridge.fetchConversationWithMessagesPaginated( + conversationId!!, + 100, + lastMessageId + ) + + if (fetchedMessages == null) { + context.shortToast("Failed to fetch messages") + return@launch + } + + if (fetchedMessages.isEmpty()) { + break + } + + fetchedMessages.forEach { + deleteIndividualMessage(it.serverMessageId) + } + + lastMessageId = fetchedMessages.first().clientMessageId + } while (true) + }.apply { + invokeOnCompletion { + messageDeleteJob = null + context.shortToast("Successfully deleted $deletedMessageCount messages") + } + } + }) + } + } + + IconButton(onClick = { + deleteAllConfirmationDialog = true + }) { + Icon( + imageVector = Icons.Filled.DeleteForever, + contentDescription = "Delete" + ) + } + } + } @Composable private fun ConversationPreview() { + DisposableEffect(Unit) { + onDispose { + selectedMessages.clear() + } + } + LazyColumn( modifier = Modifier .fillMaxSize(), @@ -64,12 +219,29 @@ class MessagingPreview( } } items(messageSize) {index -> + val elementKey = remember(index) { messages.entries.elementAt(index).key } val messageReader = ProtoReader(messages.entries.elementAt(index).value.content) val contentType = ContentType.fromMessageContainer(messageReader) Card( modifier = Modifier .padding(5.dp) + .pointerInput(Unit) { + if (contentType == ContentType.STATUS) return@pointerInput + detectTapGestures( + onLongPress = { + toggleSelectedMessage(elementKey) + }, + onTap = { + if (selectedMessages.isNotEmpty()) { + toggleSelectedMessage(elementKey) + } + } + ) + }, + colors = CardDefaults.cardColors( + containerColor = if (selectedMessages.contains(elementKey)) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant + ), ) { Row( modifier = Modifier diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -47,6 +47,7 @@ class SocialSection : Section() { } private var currentScopeContent: ScopeContent? = null + private var currentMessagingPreview by mutableStateOf(null as MessagingPreview?) private val addFriendDialog by lazy { AddFriendDialog(context, this) @@ -83,12 +84,16 @@ class SocialSection : Section() { } } - composable(MESSAGING_PREVIEW_ROUTE) { - val id = it.arguments?.getString("id") ?: return@composable - val scope = it.arguments?.getString("scope") ?: return@composable - remember { + composable(MESSAGING_PREVIEW_ROUTE) { navBackStackEntry -> + val id = navBackStackEntry.arguments?.getString("id") ?: return@composable + val scope = navBackStackEntry.arguments?.getString("scope") ?: return@composable + val messagePreview = remember { MessagingPreview(context, SocialScope.getByName(scope), id) - }.Content() + } + LaunchedEffect(key1 = id) { + currentMessagingPreview = messagePreview + } + messagePreview.Content() } } } @@ -112,6 +117,10 @@ class SocialSection : Section() { } } + if (currentRoute == MESSAGING_PREVIEW_ROUTE) { + currentMessagingPreview?.TopBarAction() + } + if (currentRoute == SocialScope.FRIEND.tabRoute || currentRoute == SocialScope.GROUP.tabRoute) { IconButton( onClick = { deleteConfirmDialog = true }, diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/MessagingBridge.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/MessagingBridge.aidl @@ -4,13 +4,15 @@ import java.util.List; import me.rhunk.snapenhance.bridge.snapclient.types.Message; interface MessagingBridge { + String getMyUserId(); + @nullable Message fetchMessage(String conversationId, String clientMessageId); @nullable Message fetchMessageByServerId(String conversationId, String serverMessageId); @nullable List<Message> fetchConversationWithMessagesPaginated(String conversationId, int limit, long beforeMessageId); - @nullable String updateMessage(String conversationId, String clientMessageId, String messageUpdate); + @nullable String updateMessage(String conversationId, long clientMessageId, String messageUpdate); @nullable String getOneToOneConversationId(String userId); } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt @@ -28,6 +28,7 @@ class CoreMessagingBridge( private val context: ModContext ) : MessagingBridge.Stub() { private val conversationManager get() = context.feature(Messaging::class).conversationManager + override fun getMyUserId() = context.database.myUserId override fun fetchMessage(conversationId: String, clientMessageId: String): Message? { return runBlocking { @@ -116,7 +117,7 @@ class CoreMessagingBridge( override fun updateMessage( conversationId: String, - clientMessageId: String, + clientMessageId: Long, messageUpdate: String ): String? { return runBlocking {