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
|
package me.rhunk.snapenhance.ui.manager.sections.social
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.DeleteForever
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
import me.rhunk.snapenhance.bridge.snapclient.types.Message
|
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.data.SocialScope
|
||||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||||
import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||||
|
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||||
|
import me.rhunk.snapenhance.ui.util.Dialog
|
||||||
|
|
||||||
class MessagingPreview(
|
class MessagingPreview(
|
||||||
private val context: RemoteSideContext,
|
private val context: RemoteSideContext,
|
||||||
private val scope: SocialScope,
|
private val scope: SocialScope,
|
||||||
private val scopeId: String
|
private val scopeId: String
|
||||||
) {
|
) {
|
||||||
|
private val alertDialogs by lazy { AlertDialogs(context.translation) }
|
||||||
|
|
||||||
private lateinit var coroutineScope: CoroutineScope
|
private lateinit var coroutineScope: CoroutineScope
|
||||||
private lateinit var messagingBridge: MessagingBridge
|
private lateinit var messagingBridge: MessagingBridge
|
||||||
private lateinit var previewScrollState: LazyListState
|
private lateinit var previewScrollState: LazyListState
|
||||||
|
private val myUserId by lazy { messagingBridge.myUserId }
|
||||||
|
|
||||||
private var conversationId: String? = null
|
private var conversationId: String? = null
|
||||||
private val messages = sortedMapOf<Long, Message>()
|
private val messages = sortedMapOf<Long, Message>()
|
||||||
private var messageSize by mutableIntStateOf(0)
|
private var messageSize by mutableIntStateOf(0)
|
||||||
private var lastMessageId = Long.MAX_VALUE
|
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
|
@Composable
|
||||||
private fun ConversationPreview() {
|
private fun ConversationPreview() {
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
selectedMessages.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
@ -64,12 +219,29 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(messageSize) {index ->
|
items(messageSize) {index ->
|
||||||
|
val elementKey = remember(index) { messages.entries.elementAt(index).key }
|
||||||
val messageReader = ProtoReader(messages.entries.elementAt(index).value.content)
|
val messageReader = ProtoReader(messages.entries.elementAt(index).value.content)
|
||||||
val contentType = ContentType.fromMessageContainer(messageReader)
|
val contentType = ContentType.fromMessageContainer(messageReader)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(5.dp)
|
.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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -47,6 +47,7 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var currentScopeContent: ScopeContent? = null
|
private var currentScopeContent: ScopeContent? = null
|
||||||
|
private var currentMessagingPreview by mutableStateOf(null as MessagingPreview?)
|
||||||
|
|
||||||
private val addFriendDialog by lazy {
|
private val addFriendDialog by lazy {
|
||||||
AddFriendDialog(context, this)
|
AddFriendDialog(context, this)
|
||||||
@ -83,12 +84,16 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(MESSAGING_PREVIEW_ROUTE) {
|
composable(MESSAGING_PREVIEW_ROUTE) { navBackStackEntry ->
|
||||||
val id = it.arguments?.getString("id") ?: return@composable
|
val id = navBackStackEntry.arguments?.getString("id") ?: return@composable
|
||||||
val scope = it.arguments?.getString("scope") ?: return@composable
|
val scope = navBackStackEntry.arguments?.getString("scope") ?: return@composable
|
||||||
remember {
|
val messagePreview = remember {
|
||||||
MessagingPreview(context, SocialScope.getByName(scope), id)
|
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) {
|
if (currentRoute == SocialScope.FRIEND.tabRoute || currentRoute == SocialScope.GROUP.tabRoute) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { deleteConfirmDialog = true },
|
onClick = { deleteConfirmDialog = true },
|
||||||
|
@ -4,13 +4,15 @@ import java.util.List;
|
|||||||
import me.rhunk.snapenhance.bridge.snapclient.types.Message;
|
import me.rhunk.snapenhance.bridge.snapclient.types.Message;
|
||||||
|
|
||||||
interface MessagingBridge {
|
interface MessagingBridge {
|
||||||
|
String getMyUserId();
|
||||||
|
|
||||||
@nullable Message fetchMessage(String conversationId, String clientMessageId);
|
@nullable Message fetchMessage(String conversationId, String clientMessageId);
|
||||||
|
|
||||||
@nullable Message fetchMessageByServerId(String conversationId, String serverMessageId);
|
@nullable Message fetchMessageByServerId(String conversationId, String serverMessageId);
|
||||||
|
|
||||||
@nullable List<Message> fetchConversationWithMessagesPaginated(String conversationId, int limit, long beforeMessageId);
|
@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);
|
@nullable String getOneToOneConversationId(String userId);
|
||||||
}
|
}
|
@ -28,6 +28,7 @@ class CoreMessagingBridge(
|
|||||||
private val context: ModContext
|
private val context: ModContext
|
||||||
) : MessagingBridge.Stub() {
|
) : MessagingBridge.Stub() {
|
||||||
private val conversationManager get() = context.feature(Messaging::class).conversationManager
|
private val conversationManager get() = context.feature(Messaging::class).conversationManager
|
||||||
|
override fun getMyUserId() = context.database.myUserId
|
||||||
|
|
||||||
override fun fetchMessage(conversationId: String, clientMessageId: String): Message? {
|
override fun fetchMessage(conversationId: String, clientMessageId: String): Message? {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
@ -116,7 +117,7 @@ class CoreMessagingBridge(
|
|||||||
|
|
||||||
override fun updateMessage(
|
override fun updateMessage(
|
||||||
conversationId: String,
|
conversationId: String,
|
||||||
clientMessageId: String,
|
clientMessageId: Long,
|
||||||
messageUpdate: String
|
messageUpdate: String
|
||||||
): String? {
|
): String? {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
|
Reference in New Issue
Block a user