feat(core): mark snaps as seen

This commit is contained in:
rhunk
2023-11-08 01:43:47 +01:00
parent c357825dc7
commit 8823093b30
4 changed files with 82 additions and 6 deletions

View File

@ -612,6 +612,7 @@
"auto_download": "\u2B07\uFE0F Auto Download", "auto_download": "\u2B07\uFE0F Auto Download",
"auto_save": "\uD83D\uDCAC Auto Save Messages", "auto_save": "\uD83D\uDCAC Auto Save Messages",
"stealth": "\uD83D\uDC7B Stealth Mode", "stealth": "\uD83D\uDC7B Stealth Mode",
"mark_as_seen": "\uD83D\uDC40 Mark Snaps as seen",
"conversation_info": "\uD83D\uDC64 Conversation Info", "conversation_info": "\uD83D\uDC64 Conversation Info",
"e2e_encryption": "\uD83D\uDD12 Use E2E Encryption" "e2e_encryption": "\uD83D\uDD12 Use E2E Encryption"
}, },
@ -710,6 +711,7 @@
}, },
"friend_menu_option": { "friend_menu_option": {
"mark_as_seen": "Mark Snaps as seen",
"preview": "Preview", "preview": "Preview",
"stealth_mode": "Stealth Mode", "stealth_mode": "Stealth Mode",
"auto_download_blacklist": "Auto Download Blacklist", "auto_download_blacklist": "Auto Download Blacklist",

View File

@ -19,7 +19,7 @@ class UserInterfaceTweaks : ConfigContainer() {
} }
val friendFeedMenuButtons = multiple( val friendFeedMenuButtons = multiple(
"friend_feed_menu_buttons","conversation_info", *MessagingRuleType.entries.filter { it.showInFriendMenu }.map { it.key }.toTypedArray() "friend_feed_menu_buttons","conversation_info", "mark_as_seen", *MessagingRuleType.entries.filter { it.showInFriendMenu }.map { it.key }.toTypedArray()
).apply { ).apply {
set(mutableListOf("conversation_info", MessagingRuleType.STEALTH.key)) set(mutableListOf("conversation_info", MessagingRuleType.STEALTH.key))
} }

View File

@ -5,18 +5,20 @@ import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent
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.util.EvictingMap
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager
import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
var conversationManager: ConversationManager? = null var conversationManager: ConversationManager? = null
private set private set
var openedConversationUUID: SnapUUID? = null var openedConversationUUID: SnapUUID? = null
private set private set
var lastFetchConversationUserUUID: SnapUUID? = null var lastFetchConversationUserUUID: SnapUUID? = null
@ -27,8 +29,10 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
var lastFocusedMessageId: Long = -1 var lastFocusedMessageId: Long = -1
private set private set
private val feedCachedSnapMessages = EvictingMap<String, List<Long>>(100)
override fun init() { override fun init() {
Hooker.hookConstructor(context.classCache.conversationManager, HookStage.BEFORE) { param -> context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
conversationManager = ConversationManager(context, param.thisObject()) conversationManager = ConversationManager(context, param.thisObject())
context.messagingBridge.triggerSessionStart() context.messagingBridge.triggerSessionStart()
context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run { context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run {
@ -37,6 +41,8 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
fun getFeedCachedMessageIds(conversationId: String) = feedCachedSnapMessages[conversationId]
override fun onActivityCreate() { override fun onActivityCreate() {
context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings -> context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings ->
findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param -> findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param ->
@ -51,6 +57,19 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
val myUserId = context.database.myUserId
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
feedCachedSnapMessages[conversationId] = messages.filter { msg ->
msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true
}.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId }
}
context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param -> context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param ->
val userIdToConversation = (param.arg<ArrayList<*>>(0)) val userIdToConversation = (param.arg<ArrayList<*>>(0))
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@ -96,12 +115,12 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
lastFocusedMessageId = event.messageId lastFocusedMessageId = event.messageId
} }
Hooker.hook(context.classCache.conversationManager, "fetchMessage", HookStage.BEFORE) { param -> context.classCache.conversationManager.hook("fetchMessage", HookStage.BEFORE) { param ->
lastFetchConversationUserUUID = SnapUUID((param.arg(0) as Any)) lastFetchConversationUserUUID = SnapUUID((param.arg(0) as Any))
lastFocusedMessageId = param.arg(1) lastFocusedMessageId = param.arg(1)
} }
Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, { context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, {
hideTypingNotification || stealthMode.canUseRule(openedConversationUUID.toString()) hideTypingNotification || stealthMode.canUseRule(openedConversationUUID.toString())
}) { }) {
it.setResult(null) it.setResult(null)

View File

@ -9,9 +9,15 @@ 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.CompoundButton
import android.widget.ProgressBar
import android.widget.Switch import android.widget.Switch
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.database.impl.UserConversationLink
@ -29,6 +35,9 @@ 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 {
@ -101,6 +110,44 @@ class FriendFeedInfoMenu : AbstractMenu() {
} }
} }
private fun markAsSeen(conversationId: String) {
val messaging = context.feature(Messaging::class)
val messageIds = messaging.getFeedCachedMessageIds(conversationId)?.takeIf { it.isNotEmpty() } ?: run {
context.shortToast("No recent snaps found")
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)
@ -253,5 +300,13 @@ class FriendFeedInfoMenu : AbstractMenu() {
{ ruleFeature.setState(conversationId, it) } { ruleFeature.setState(conversationId, it) }
) )
} }
if (friendFeedMenuOptions.contains("mark_as_seen")) {
viewConsumer(Button(view.context).apply {
text = modContext.translation["friend_menu_option.mark_as_seen"]
applyTheme(view.width, hasRadius = true)
setOnClickListener { markAsSeen(conversationId) }
})
}
} }
} }