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,34 +79,45 @@ 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) {
.setTitle("...") ViewAppearanceHelper.newAlertDialogBuilder(ctx)
.setView(ProgressBar(ctx)) .setTitle("...")
.setCancelable(false) .setView(LinearLayout(ctx).apply {
.show() 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)
.show()
}
return context.coroutineScope.launch { ids.forEachIndexed { index, id ->
ids.forEach { id -> launch(Dispatchers.Main) {
runCatching { dialog.setTitle(
action(id) translation.format("progress_status", "index" to (index + 1).toString(), "total" to ids.size.toString())
}.onFailure { )
context.log.error("Failed to process $it", it)
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))
} }
withContext(Dispatchers.Main) { runCatching {
dialog.dismiss() action(id) {
launch(Dispatchers.Main) {
statusTextView.text = it
}
}
}.onFailure {
context.log.error("Failed to process $it", it)
context.shortToast("Failed to process $id")
} }
delay(Random.nextLong(delay.first, delay.second))
}
withContext(Dispatchers.Main) {
dialog.dismiss()
} }
} }
@ -378,20 +396,22 @@ 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) }
} friendInfo.snapScore.takeIf { it > 0 }?.let {
remember(friendInfo) { friendInfo.snapScore.takeIf { it > 0 } }?.let { append("\nSnap Score: $it")
Text(text = "Snap Score: $it", fontSize = 12.sp, fontWeight = FontWeight.Light) }
} friendInfo.streakLength.takeIf { it > 0 }?.let {
remember(friendInfo) { friendInfo.streakLength.takeIf { it > 0 } }?.let { append("\nStreaks length: $it")
Text(text = "Streaks length: $it", fontSize = 12.sp, fontWeight = FontWeight.Light) }
}
} }
Text(text = userInfo, fontSize = 12.sp, fontWeight = FontWeight.Light, lineHeight = 16.sp, overflow = TextOverflow.Ellipsis)
} }
Checkbox( Checkbox(
@ -421,56 +441,65 @@ class BulkMessagingAction : AbstractAction() {
val ctx = LocalContext.current val ctx = LocalContext.current
val actions = remember {
mapOf<() -> String, () -> Unit>(
{ "Clean " + selectedFriends.size + " conversations" } to {
context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also {
selectedFriends.clear()
}, onError = { error ->
context.shortToast("Failed to fetch conversations: $error")
}, onSuccess = { conversations ->
removeAction(ctx, conversations.map { it.second }.distinct(), delay = 10L to 40L) { conversationId, setDialogMessage ->
cleanConversation(
conversationId, setDialogMessage
)
}.invokeOnCompletion {
coroutineScope.launch { refreshList() }
}
})
},
{ "Remove " + selectedFriends.size + " friends" } to {
removeAction(ctx, selectedFriends.toList().also {
selectedFriends.clear()
}, delay = 500L to 1200L) { userId, _ -> removeFriend(userId) }.invokeOnCompletion {
coroutineScope.launch { refreshList() }
}
},
{ "Clean " + selectedFriends.size + " conversations and remove " + selectedFriends.size + " friends" } to {
context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also {
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( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Button( actions.forEach { (text, actionFunction) ->
modifier = Modifier Button(
.fillMaxWidth() modifier = Modifier
.padding(2.dp), .fillMaxWidth()
onClick = { .padding(2.dp),
showConfirmationDialog = true onClick = {
action = { showConfirmationDialog = true
val messaging = context.feature(Messaging::class) action = actionFunction
messaging.conversationManager?.apply { },
getOneOnOneConversationIds(selectedFriends, onError = { error -> enabled = selectedFriends.isNotEmpty()
context.shortToast("Failed to fetch conversations: $error") ) {
}, onSuccess = { conversations -> Text(text = remember(selectedFriends.size) { text() })
context.runOnUiThread { }
removeAction(ctx, conversations.map { it.second }.distinct(), delay = 100L to 400L) {
messaging.clearConversationFromFeed(it, onError = { error ->
context.shortToast("Failed to clear conversation: $error")
})
}.invokeOnCompletion {
coroutineScope.launch { refreshList() }
}
}
})
selectedFriends.clear()
}
}
},
enabled = selectedFriends.isNotEmpty()
) {
Text(text = "Clear " + selectedFriends.size + " conversations")
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp),
onClick = {
showConfirmationDialog = true
action = {
removeAction(ctx, selectedFriends.toList().also {
selectedFriends.clear()
}, delay = 500L to 1200L) { removeFriend(it) }.invokeOnCompletion {
coroutineScope.launch { refreshList() }
}
}
},
enabled = selectedFriends.isNotEmpty()
) {
Text(text = "Remove " + selectedFriends.size + " friends")
} }
} }
} }
@ -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()
}
} }