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:
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 {