feat(core): bulk clean conversations

This commit is contained in:
rhunk 2024-04-26 16:34:14 +02:00
parent b92378bffd
commit 7fc3ec9d10
3 changed files with 146 additions and 95 deletions

View File

@ -33,12 +33,12 @@ import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.ReceiversConfig import me.rhunk.snapenhance.common.ReceiversConfig
import me.rhunk.snapenhance.common.data.ContentType 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.messaging.MessagingConstraints
import me.rhunk.snapenhance.common.messaging.MessagingTask
import me.rhunk.snapenhance.common.messaging.MessagingTaskConstraint
import me.rhunk.snapenhance.common.messaging.MessagingTaskType
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.messaging.MessagingConstraints
import me.rhunk.snapenhance.messaging.MessagingTask
import me.rhunk.snapenhance.messaging.MessagingTaskConstraint
import me.rhunk.snapenhance.messaging.MessagingTaskType
import me.rhunk.snapenhance.ui.manager.Routes import me.rhunk.snapenhance.ui.manager.Routes
import me.rhunk.snapenhance.ui.util.Dialog import me.rhunk.snapenhance.ui.util.Dialog
@ -299,14 +299,17 @@ class MessagingPreview: Routes.Route() {
else selectConstraintsDialog = true else selectConstraintsDialog = true
} }
ActionButton(text = translation[if (hasSelection) "mark_selection_as_seen_option" else "mark_all_as_seen_option"], icon = Icons.Rounded.RemoveRedEye) { ActionButton(text = translation[if (hasSelection) "mark_selection_as_seen_option" else "mark_all_as_seen_option"], icon = Icons.Rounded.RemoveRedEye) {
launchMessagingTask(MessagingTaskType.READ, listOf( launchMessagingTask(
MessagingTaskType.READ, listOf(
MessagingConstraints.NO_USER_ID(messagingBridge.myUserId), MessagingConstraints.NO_USER_ID(messagingBridge.myUserId),
MessagingConstraints.CONTENT_TYPE(arrayOf(ContentType.SNAP)) MessagingConstraints.CONTENT_TYPE(arrayOf(ContentType.SNAP))
)) ))
runCurrentTask() runCurrentTask()
} }
ActionButton(text = translation[if (hasSelection) "delete_selection_option" else "delete_all_option"], icon = Icons.Rounded.DeleteForever) { ActionButton(text = translation[if (hasSelection) "delete_selection_option" else "delete_all_option"], icon = Icons.Rounded.DeleteForever) {
launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(messagingBridge.myUserId))) { message -> launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(messagingBridge.myUserId), {
contentType != ContentType.STATUS.id
})) { message ->
coroutineScope.launch { coroutineScope.launch {
message.contentType = ContentType.STATUS.id message.contentType = ContentType.STATUS.id
} }

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.messaging package me.rhunk.snapenhance.common.messaging
import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.MutableIntState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -67,9 +67,9 @@ class MessagingTask(
error?.takeIf { error != "DUPLICATE_REQUEST" }?.let { error?.takeIf { error != "DUPLICATE_REQUEST" }?.let {
onFailure(message, error) onFailure(message, error)
} }
onSuccess(message)
processedMessageCount.intValue++ processedMessageCount.intValue++
delay(Random.nextLong(20, 50)) onSuccess(message)
delay(Random.nextLong(50, 80))
} }
} }

View File

@ -3,7 +3,11 @@ package me.rhunk.snapenhance.core.action.impl
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.view.Gravity
import android.view.View
import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -27,12 +31,15 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.FriendLinkType import me.rhunk.snapenhance.common.data.FriendLinkType
import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.FriendInfo
import me.rhunk.snapenhance.common.messaging.MessagingConstraints
import me.rhunk.snapenhance.common.messaging.MessagingTask
import me.rhunk.snapenhance.common.messaging.MessagingTaskType
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.util.ktx.copyToClipboard import me.rhunk.snapenhance.common.util.ktx.copyToClipboard
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
@ -72,36 +79,47 @@ class BulkMessagingAction : AbstractAction() {
ctx: Context, ctx: Context,
ids: List<String>, ids: List<String>,
delay: Pair<Long, Long>, delay: Pair<Long, Long>,
action: (String) -> Unit = {}, action: suspend (id: String, setDialogMessage: (String) -> Unit) -> Unit = { _, _ -> }
): Job { ) = context.coroutineScope.launch {
var index = 0 val statusTextView = TextView(ctx)
val dialog = ViewAppearanceHelper.newAlertDialogBuilder(ctx) val dialog = withContext(Dispatchers.Main) {
ViewAppearanceHelper.newAlertDialogBuilder(ctx)
.setTitle("...") .setTitle("...")
.setView(ProgressBar(ctx)) .setView(LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
addView(statusTextView.apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
textAlignment = View.TEXT_ALIGNMENT_CENTER
})
addView(ProgressBar(ctx))
})
.setCancelable(false) .setCancelable(false)
.show() .show()
}
return context.coroutineScope.launch { ids.forEachIndexed { index, id ->
ids.forEach { id -> launch(Dispatchers.Main) {
dialog.setTitle(
translation.format("progress_status", "index" to (index + 1).toString(), "total" to ids.size.toString())
)
}
runCatching { runCatching {
action(id) action(id) {
launch(Dispatchers.Main) {
statusTextView.text = it
}
}
}.onFailure { }.onFailure {
context.log.error("Failed to process $it", it) context.log.error("Failed to process $it", it)
context.shortToast("Failed to process $id") context.shortToast("Failed to process $id")
} }
index++
withContext(Dispatchers.Main) {
dialog.setTitle(
translation.format("progress_status", "index" to index.toString(), "total" to ids.size.toString())
)
}
delay(Random.nextLong(delay.first, delay.second)) delay(Random.nextLong(delay.first, delay.second))
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
dialog.dismiss() dialog.dismiss()
} }
} }
}
@Composable @Composable
private fun ConfirmationDialog( private fun ConfirmationDialog(
@ -378,21 +396,23 @@ class BulkMessagingAction : AbstractAction() {
Text(text = (friendInfo.displayName ?: friendInfo.mutableUsername).toString(), fontSize = 16.sp, fontWeight = FontWeight.Bold, overflow = TextOverflow.Ellipsis, maxLines = 1) Text(text = (friendInfo.displayName ?: friendInfo.mutableUsername).toString(), fontSize = 16.sp, fontWeight = FontWeight.Bold, overflow = TextOverflow.Ellipsis, maxLines = 1)
Text(text = friendInfo.mutableUsername.toString(), fontSize = 10.sp, fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 1) Text(text = friendInfo.mutableUsername.toString(), fontSize = 10.sp, fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 1)
} }
Text(text = "Relationship: " + remember(friendInfo) { val userInfo = remember(friendInfo) {
context.translation["friendship_link_type.${FriendLinkType.fromValue(friendInfo.friendLinkType).shortName}"] buildString {
}, fontSize = 12.sp, fontWeight = FontWeight.Light) append("Relationship: ")
remember(friendInfo) { friendInfo.addedTimestamp.takeIf { it > 0L }?.let { append(context.translation["friendship_link_type.${FriendLinkType.fromValue(friendInfo.friendLinkType).shortName}"])
DateFormat.getDateTimeInstance().format(Date(friendInfo.addedTimestamp)) friendInfo.addedTimestamp.takeIf { it > 0L }?.let {
} }?.let { append("\nAdded ${DateFormat.getDateTimeInstance().format(Date(it))}")
Text(text = "Added $it", fontSize = 12.sp, fontWeight = FontWeight.Light)
} }
remember(friendInfo) { friendInfo.snapScore.takeIf { it > 0 } }?.let { friendInfo.snapScore.takeIf { it > 0 }?.let {
Text(text = "Snap Score: $it", fontSize = 12.sp, fontWeight = FontWeight.Light) append("\nSnap Score: $it")
} }
remember(friendInfo) { friendInfo.streakLength.takeIf { it > 0 } }?.let { friendInfo.streakLength.takeIf { it > 0 }?.let {
Text(text = "Streaks length: $it", fontSize = 12.sp, fontWeight = FontWeight.Light) append("\nStreaks length: $it")
} }
} }
}
Text(text = userInfo, fontSize = 12.sp, fontWeight = FontWeight.Light, lineHeight = 16.sp, overflow = TextOverflow.Ellipsis)
}
Checkbox( Checkbox(
checked = selectedFriends.contains(friendInfo.userId), checked = selectedFriends.contains(friendInfo.userId),
@ -421,56 +441,65 @@ class BulkMessagingAction : AbstractAction() {
val ctx = LocalContext.current val ctx = LocalContext.current
Column( val actions = remember {
modifier = Modifier.fillMaxWidth(), mapOf<() -> String, () -> Unit>(
) { { "Clean " + selectedFriends.size + " conversations" } to {
Button( context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also {
modifier = Modifier selectedFriends.clear()
.fillMaxWidth() }, onError = { error ->
.padding(2.dp),
onClick = {
showConfirmationDialog = true
action = {
val messaging = context.feature(Messaging::class)
messaging.conversationManager?.apply {
getOneOnOneConversationIds(selectedFriends, onError = { error ->
context.shortToast("Failed to fetch conversations: $error") context.shortToast("Failed to fetch conversations: $error")
}, onSuccess = { conversations -> }, onSuccess = { conversations ->
context.runOnUiThread { removeAction(ctx, conversations.map { it.second }.distinct(), delay = 10L to 40L) { conversationId, setDialogMessage ->
removeAction(ctx, conversations.map { it.second }.distinct(), delay = 100L to 400L) { cleanConversation(
messaging.clearConversationFromFeed(it, onError = { error -> conversationId, setDialogMessage
context.shortToast("Failed to clear conversation: $error") )
})
}.invokeOnCompletion { }.invokeOnCompletion {
coroutineScope.launch { refreshList() } coroutineScope.launch { refreshList() }
} }
}
}) })
},
{ "Remove " + selectedFriends.size + " friends" } to {
removeAction(ctx, selectedFriends.toList().also {
selectedFriends.clear() selectedFriends.clear()
} }, delay = 500L to 1200L) { userId, _ -> removeFriend(userId) }.invokeOnCompletion {
coroutineScope.launch { refreshList() }
} }
}, },
enabled = selectedFriends.isNotEmpty() { "Clean " + selectedFriends.size + " conversations and remove " + selectedFriends.size + " friends" } to {
) { context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also {
Text(text = "Clear " + selectedFriends.size + " conversations") selectedFriends.clear()
}, onError = { error ->
context.shortToast("Failed to fetch conversations: $error")
}, onSuccess = { conversations ->
removeAction(ctx, conversations.map { it.second }.distinct(), delay = 500L to 1200L) { conversationId, setDialogMessage ->
cleanConversation(
conversationId, setDialogMessage
)
removeFriend(conversations.firstOrNull { it.second == conversationId }?.first ?: return@removeAction)
}.invokeOnCompletion {
coroutineScope.launch { refreshList() }
} }
})
}
)
}
Column(
modifier = Modifier.fillMaxWidth(),
) {
actions.forEach { (text, actionFunction) ->
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(2.dp), .padding(2.dp),
onClick = { onClick = {
showConfirmationDialog = true showConfirmationDialog = true
action = { action = actionFunction
removeAction(ctx, selectedFriends.toList().also {
selectedFriends.clear()
}, delay = 500L to 1200L) { removeFriend(it) }.invokeOnCompletion {
coroutineScope.launch { refreshList() }
}
}
}, },
enabled = selectedFriends.isNotEmpty() enabled = selectedFriends.isNotEmpty()
) { ) {
Text(text = "Remove " + selectedFriends.size + " friends") Text(text = remember(selectedFriends.size) { text() })
}
} }
} }
} }
@ -520,4 +549,23 @@ class BulkMessagingAction : AbstractAction() {
}.invoke(completable) }.invoke(completable)
} }
} }
private suspend fun cleanConversation(
conversationId: String,
setDialogMessage: (String) -> Unit
) {
val messageCount = mutableIntStateOf(0)
MessagingTask(
context.messagingBridge,
conversationId,
taskType = MessagingTaskType.DELETE,
constraints = listOf(MessagingConstraints.MY_USER_ID(context.messagingBridge), {
contentType != ContentType.STATUS.id
}),
processedMessageCount = messageCount,
onSuccess = {
setDialogMessage("${messageCount.intValue} deleted messages")
},
).run()
}
} }