mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat(manager/social): chat purge
This commit is contained in:
@ -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
|
||||
|
@ -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 },
|
||||
|
@ -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);
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user