feat(auto_mark_as_read): snap reply

This commit is contained in:
rhunk 2024-04-17 17:43:35 +02:00
parent 6e7aa7c498
commit 785fb49aa9
5 changed files with 88 additions and 61 deletions

View File

@ -490,7 +490,7 @@
},
"auto_mark_as_read": {
"name": "Auto Mark as Read",
"description": "Automatically marks messages as read when sending a message to a conversation, even when Stealth Mode is enabled"
"description": "Automatically marks messages/snaps as read even when Stealth Mode is enabled"
},
"loop_media_playback": {
"name": "Loop Media Playback",
@ -1092,6 +1092,10 @@
"location_indicator": "Adds a \uD83D\uDCCD icon to snaps when they have been sent with location enabled",
"ovf_editor_indicator": "Indicates if a snap has been sent using OVF Editor",
"director_mode_indicator": "Adds a \u270F\uFE0F icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps"
},
"auto_mark_as_read": {
"conversation_read": "Mark conversation as read when sending a message",
"snap_reply": "Mark snaps as read when replying to them"
}
}
},

View File

@ -55,7 +55,7 @@ class MessagingTweaks : ConfigContainer() {
val hideBitmojiPresence = boolean("hide_bitmoji_presence")
val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val autoMarkAsRead = boolean("auto_mark_as_read") { requireRestart() }
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read") { requireRestart() }
val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }
val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}

View File

@ -1,12 +1,26 @@
package me.rhunk.snapenhance.core.features.impl.messaging
import android.widget.ProgressBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.WarningAmber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
class AutoMarkAsRead : Feature("Auto Mark As Read", loadParams = FeatureLoadParams.INIT_SYNC) {
val isEnabled by lazy { context.config.messaging.autoMarkAsRead.get() }
val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") }
fun markConversationsAsRead(conversationIds: List<String>) {
conversationIds.forEach { conversationId ->
@ -20,13 +34,72 @@ class AutoMarkAsRead : Feature("Auto Mark As Read", loadParams = FeatureLoadPara
}
}
private suspend fun markSnapAsSeen(conversationId: String, clientMessageId: Long) {
suspendCoroutine { continuation ->
context.feature(Messaging::class).conversationManager?.updateMessage(conversationId, clientMessageId, MessageUpdate.READ) {
continuation.resume(Unit)
if (it != null && it != "DUPLICATEREQUEST") {
context.log.error("Error marking message as read $it")
}
}
}
}
fun markSnapsAsSeen(conversationId: String) {
val messaging = context.feature(Messaging::class)
val messageIds = messaging.getFeedCachedMessageIds(conversationId)?.takeIf { it.isNotEmpty() } ?: run {
context.inAppOverlay.showStatusToast(
Icons.Default.WarningAmber,
context.translation["mark_as_seen.no_unseen_snaps_toast"]
)
return
}
var job: Job? = null
val dialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle("Processing...")
.setView(ProgressBar(context.mainActivity).apply {
setPadding(10, 10, 10, 10)
})
.setOnDismissListener { job?.cancel() }
.show()
context.coroutineScope.launch(Dispatchers.IO) {
messageIds.forEach { messageId ->
markSnapAsSeen(conversationId, messageId)
delay(Random.nextLong(20, 60))
context.runOnUiThread {
dialog.setTitle("Processing... (${messageIds.indexOf(messageId) + 1}/${messageIds.size})")
}
}
}.also { job = it }.invokeOnCompletion {
context.runOnUiThread {
dialog.dismiss()
}
}
}
override fun init() {
if (!isEnabled) return
val config by context.config.messaging.autoMarkAsRead
if (config.isEmpty()) return
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
event.addCallbackResult("onSuccess") {
if (canMarkConversationAsRead) {
markConversationsAsRead(event.destinations.conversations?.map { it.toString() } ?: return@addCallbackResult)
}
if (config.contains("snap_reply")) {
val quotedMessageId = event.messageContent.instanceNonNull().getObjectFieldOrNull("mQuotedMessageId") as? Long ?: return@addCallbackResult
val message = context.database.getConversationMessageFromId(quotedMessageId) ?: return@addCallbackResult
if (message.contentType == ContentType.SNAP.id) {
context.coroutineScope.launch {
markSnapAsSeen(event.destinations.conversations?.firstOrNull()?.toString() ?: return@launch, quotedMessageId)
}
}
}
}
}
}
}

View File

@ -201,7 +201,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}, onSuccess = {
context.coroutineScope.launch(coroutineDispatcher) {
appendNotificationText("${myUser.displayName ?: myUser.mutableUsername}: $input")
context.feature(AutoMarkAsRead::class).takeIf { it.isEnabled }?.markConversationsAsRead(listOf(conversationId))
context.feature(AutoMarkAsRead::class).takeIf { it.canMarkConversationAsRead }?.markConversationsAsRead(listOf(conversationId))
}
})
}

View File

@ -8,30 +8,21 @@ import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Switch
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircleOutline
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.NotInterested
import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.FriendLinkType
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
import me.rhunk.snapenhance.common.database.impl.FriendInfo
import me.rhunk.snapenhance.common.database.impl.UserConversationLink
import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
@ -39,6 +30,7 @@ import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption
import me.rhunk.snapenhance.core.features.impl.messaging.AutoMarkAsRead
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
@ -53,9 +45,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
class FriendFeedInfoMenu : AbstractMenu() {
private fun getImageDrawable(url: String): Drawable {
@ -131,47 +120,6 @@ class FriendFeedInfoMenu : AbstractMenu() {
}
}
private fun markAsSeen(conversationId: String) {
val messaging = context.feature(Messaging::class)
val messageIds = messaging.getFeedCachedMessageIds(conversationId)?.takeIf { it.isNotEmpty() } ?: run {
context.inAppOverlay.showStatusToast(
Icons.Default.WarningAmber,
context.translation["mark_as_seen.no_unseen_snaps_toast"]
)
return
}
var job: Job? = null
val dialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle("Processing...")
.setView(ProgressBar(context.mainActivity).apply {
setPadding(10, 10, 10, 10)
})
.setOnDismissListener { job?.cancel() }
.show()
context.coroutineScope.launch(Dispatchers.IO) {
messageIds.forEach { messageId ->
suspendCoroutine { continuation ->
messaging.conversationManager?.updateMessage(conversationId, messageId, MessageUpdate.READ) {
continuation.resume(Unit)
if (it != null && it != "DUPLICATEREQUEST") {
context.log.error("Error marking message as read $it")
}
}
}
delay(Random.nextLong(20, 60))
context.runOnUiThread {
dialog.setTitle("Processing... (${messageIds.indexOf(messageId) + 1}/${messageIds.size})")
}
}
}.also { job = it }.invokeOnCompletion {
context.runOnUiThread {
dialog.dismiss()
}
}
}
private fun showPreview(userId: String?, conversationId: String) {
//query message
val messageLogger = context.feature(MessageLogger::class)
@ -324,8 +272,10 @@ class FriendFeedInfoMenu : AbstractMenu() {
isSoundEffectsEnabled = false
applyTheme(view.width, hasRadius = true)
setOnClickListener {
this@FriendFeedInfoMenu.context.mainActivity?.triggerRootCloseTouchEvent()
markAsSeen(conversationId)
this@FriendFeedInfoMenu.context.apply {
mainActivity?.triggerRootCloseTouchEvent()
feature(AutoMarkAsRead::class).markSnapsAsSeen(conversationId)
}
}
})
}