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": { "auto_mark_as_read": {
"name": "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": { "loop_media_playback": {
"name": "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", "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", "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" "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 hideBitmojiPresence = boolean("hide_bitmoji_presence")
val hideTypingNotifications = boolean("hide_typing_notifications") val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") 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 loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }
val disableReplayInFF = boolean("disable_replay_in_ff") val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()} val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}

View File

@ -1,12 +1,26 @@
package me.rhunk.snapenhance.core.features.impl.messaging 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.event.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode 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) { 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>) { fun markConversationsAsRead(conversationIds: List<String>) {
conversationIds.forEach { conversationId -> conversationIds.forEach { conversationId ->
@ -20,12 +34,71 @@ 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() { override fun init() {
if (!isEnabled) return val config by context.config.messaging.autoMarkAsRead
if (config.isEmpty()) return
context.event.subscribe(SendMessageWithContentEvent::class) { event -> context.event.subscribe(SendMessageWithContentEvent::class) { event ->
event.addCallbackResult("onSuccess") { event.addCallbackResult("onSuccess") {
markConversationsAsRead(event.destinations.conversations?.map { it.toString() } ?: return@addCallbackResult) 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 = { }, onSuccess = {
context.coroutineScope.launch(coroutineDispatcher) { context.coroutineScope.launch(coroutineDispatcher) {
appendNotificationText("${myUser.displayName ?: myUser.mutableUsername}: $input") 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.CompoundButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Switch import android.widget.Switch
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material.icons.filled.CheckCircleOutline
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.NotInterested import androidx.compose.material.icons.filled.NotInterested
import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.ContentType
import me.rhunk.snapenhance.common.data.FriendLinkType 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.ConversationMessage
import me.rhunk.snapenhance.common.database.impl.FriendInfo 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.EnumScriptInterface
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface 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.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption 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.messaging.Messaging
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
@ -53,9 +45,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
class FriendFeedInfoMenu : AbstractMenu() { class FriendFeedInfoMenu : AbstractMenu() {
private fun getImageDrawable(url: String): Drawable { 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) { private fun showPreview(userId: String?, conversationId: String) {
//query message //query message
val messageLogger = context.feature(MessageLogger::class) val messageLogger = context.feature(MessageLogger::class)
@ -324,8 +272,10 @@ class FriendFeedInfoMenu : AbstractMenu() {
isSoundEffectsEnabled = false isSoundEffectsEnabled = false
applyTheme(view.width, hasRadius = true) applyTheme(view.width, hasRadius = true)
setOnClickListener { setOnClickListener {
this@FriendFeedInfoMenu.context.mainActivity?.triggerRootCloseTouchEvent() this@FriendFeedInfoMenu.context.apply {
markAsSeen(conversationId) mainActivity?.triggerRootCloseTouchEvent()
feature(AutoMarkAsRead::class).markSnapsAsSeen(conversationId)
}
} }
}) })
} }