commit 248100b8b9c9a373e7575be4a0166d6a003099ea
parent 3f02023580470e7b71ff76f6d7aba70dd2a5232a
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Wed, 12 Jun 2024 16:04:27 +0200
feat(core): mark as seen button
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
6 files changed, 162 insertions(+), 67 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -613,6 +613,14 @@
"name": "Auto Mark as Read",
"description": "Automatically marks messages/snaps as read even when Stealth Mode is enabled"
},
+ "mark_snap_as_seen_button": {
+ "name": "Mark Snap as Seen Button",
+ "description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled"
+ },
+ "skip_when_marking_as_seen": {
+ "name": "Skip When Marking as Seen",
+ "description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button"
+ },
"loop_media_playback": {
"name": "Loop Media Playback",
"description": "Loops media playback when viewing Snaps / Stories"
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt
@@ -59,6 +59,8 @@ class MessagingTweaks : ConfigContainer() {
val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read") { requireRestart() }
+ val markSnapAsSeenButton = boolean("mark_snap_as_seen_button") { requireRestart() }
+ val skipWhenMarkingAsSeen = boolean("skip_when_marking_as_seen") { requireRestart() }
val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }
val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoMarkAsRead.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoMarkAsRead.kt
@@ -34,10 +34,10 @@ class AutoMarkAsRead : Feature("Auto Mark As Read", loadParams = FeatureLoadPara
}
}
- private suspend fun markSnapAsSeen(conversationId: String, clientMessageId: Long) {
- suspendCoroutine { continuation ->
+ suspend fun markSnapAsSeen(conversationId: String, clientMessageId: Long): String? {
+ return suspendCoroutine { continuation ->
context.feature(Messaging::class).conversationManager?.updateMessage(conversationId, clientMessageId, MessageUpdate.READ) {
- continuation.resume(Unit)
+ continuation.resume(it)
if (it != null && it != "DUPLICATEREQUEST") {
context.log.error("Error marking message as read $it")
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt
@@ -23,7 +23,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
arrayOf(
NewChatActionMenu(),
OperaContextActionMenu(),
- OperaDownloadIconMenu(),
+ OperaViewerIcons(),
SettingsGearInjector(),
FriendFeedInfoMenu(),
ChatActionMenu(),
@@ -84,7 +84,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
}
if (childView.id == contextMenuButtonIconView) {
- menuMap[OperaDownloadIconMenu::class]!!.inject(viewGroup, childView, originalAddView)
+ menuMap[OperaViewerIcons::class]!!.inject(viewGroup, childView, originalAddView)
}
if (event.parent.id == componentsHolder && childView.id == feedNewChat) {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaDownloadIconMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaDownloadIconMenu.kt
@@ -1,61 +0,0 @@
-package me.rhunk.snapenhance.core.ui.menu.impl
-
-import android.graphics.Color
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup
-import android.widget.FrameLayout
-import android.widget.ImageView
-import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
-import me.rhunk.snapenhance.core.ui.children
-import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
-import me.rhunk.snapenhance.core.util.ktx.getDimens
-import me.rhunk.snapenhance.core.util.ktx.getDrawable
-import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
-
-class OperaDownloadIconMenu : AbstractMenu() {
- private val downloadSvgDrawable by lazy { context.resources.getDrawable("svg_download", context.androidContext.theme) }
- private val actionMenuIconSize by lazy { context.resources.getDimens("action_menu_icon_size") }
- private val actionMenuIconMargin by lazy { context.resources.getDimens("action_menu_icon_margin") }
- private val actionMenuIconMarginTop by lazy { context.resources.getDimens("action_menu_icon_margin_top") }
-
- override fun inject(parent: ViewGroup, view: View, viewConsumer: (View) -> Unit) {
- if (!context.config.downloader.operaDownloadButton.get()) return
-
- val mediaDownloader = context.feature(MediaDownloader::class)
-
- parent.addView(ImageView(view.context).apply {
- setImageDrawable(downloadSvgDrawable)
- setColorFilter(Color.WHITE)
- layoutParams = FrameLayout.LayoutParams(
- actionMenuIconSize,
- actionMenuIconSize
- ).apply {
- setMargins(0, actionMenuIconMarginTop * 2 + actionMenuIconSize, 0, 0)
- marginEnd = actionMenuIconMargin
- gravity = Gravity.TOP or Gravity.END
- }
- setOnClickListener {
- mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = false)
- }
- setOnLongClickListener {
- context.vibrateLongPress()
- mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = true)
- true
- }
- addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
- override fun onViewAttachedToWindow(v: View) {
- v.visibility = View.VISIBLE
- (parent.parent as? ViewGroup)?.children()?.forEach { child ->
- if (child !is ViewGroup) return@forEach
- child.children().forEach {
- if (it::class.java.name.endsWith("PreviewToolbar")) v.visibility = View.GONE
- }
- }
- }
-
- override fun onViewDetachedFromWindow(v: View) {}
- })
- }, 0)
- }
-}-
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaViewerIcons.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaViewerIcons.kt
@@ -0,0 +1,146 @@
+package me.rhunk.snapenhance.core.ui.menu.impl
+
+import android.graphics.Color
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Info
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
+import me.rhunk.snapenhance.core.features.impl.messaging.AutoMarkAsRead
+import me.rhunk.snapenhance.core.ui.children
+import me.rhunk.snapenhance.core.ui.iterateParent
+import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
+import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
+import me.rhunk.snapenhance.core.util.ktx.getDimens
+import me.rhunk.snapenhance.core.util.ktx.getDrawable
+import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
+
+class OperaViewerIcons : AbstractMenu() {
+ private val downloadSvgDrawable by lazy { context.resources.getDrawable("svg_download", context.androidContext.theme) }
+ private val eyeSvgDrawable by lazy { context.resources.getDrawable("svg_eye_24x24", context.androidContext.theme) }
+ private val actionMenuIconSize by lazy { context.resources.getDimens("action_menu_icon_size") }
+ private val actionMenuIconMargin by lazy { context.resources.getDimens("action_menu_icon_margin") }
+ private val actionMenuIconMarginTop by lazy { context.resources.getDimens("action_menu_icon_margin_top") }
+
+ override fun inject(parent: ViewGroup, view: View, viewConsumer: (View) -> Unit) {
+ val mediaDownloader = context.feature(MediaDownloader::class)
+
+ if (context.config.downloader.operaDownloadButton.get()) {
+ parent.addView(LinearLayout(view.context).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ ).apply {
+ setMargins(0, actionMenuIconMarginTop * 2 + actionMenuIconSize, 0, 0)
+ marginEnd = actionMenuIconMargin
+ gravity = Gravity.TOP or Gravity.END
+ }
+ addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ v.visibility = View.VISIBLE
+ (parent.parent as? ViewGroup)?.children()?.forEach { child ->
+ if (child !is ViewGroup) return@forEach
+ child.children().forEach {
+ if (it::class.java.name.endsWith("PreviewToolbar")) v.visibility = View.GONE
+ }
+ }
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {}
+ })
+
+ addView(ImageView(view.context).apply {
+ setImageDrawable(downloadSvgDrawable)
+ setColorFilter(Color.WHITE)
+ layoutParams = LinearLayout.LayoutParams(
+ actionMenuIconSize,
+ actionMenuIconSize
+ ).apply {
+ setMargins(0, 0, 0, actionMenuIconMargin * 2)
+ }
+ setOnClickListener {
+ mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = false)
+ }
+ setOnLongClickListener {
+ context.vibrateLongPress()
+ mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = true)
+ true
+ }
+ })
+ }, 0)
+
+ }
+
+ if (context.config.messaging.markSnapAsSeenButton.get()) {
+ fun getMessageId(): Pair<String, String>? {
+ return mediaDownloader.lastSeenMapParams?.get("MESSAGE_ID")
+ ?.toString()
+ ?.split(":")
+ ?.takeIf { it.size == 3 }
+ ?.let { return it[0] to it[2] }
+ }
+
+ parent.addView(ImageView(view.context).apply {
+ setImageDrawable(eyeSvgDrawable)
+ setColorFilter(Color.WHITE)
+ addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ v.visibility = View.GONE
+ this@OperaViewerIcons.context.coroutineScope.launch(Dispatchers.Main) {
+ delay(300)
+ v.visibility = if (getMessageId() != null) View.VISIBLE else View.GONE
+ }
+ }
+ override fun onViewDetachedFromWindow(v: View) {}
+ })
+ layoutParams = FrameLayout.LayoutParams(
+ actionMenuIconSize,
+ actionMenuIconSize
+ ).apply {
+ setMargins(0, 0, 0, actionMenuIconMarginTop * 2 + (80 * context.resources.displayMetrics.density).toInt())
+ marginEnd = actionMenuIconMarginTop * 2
+ marginStart = actionMenuIconMarginTop * 2
+ gravity = Gravity.BOTTOM or Gravity.END
+ }
+ setOnClickListener {
+ this@OperaViewerIcons.context.apply {
+ coroutineScope.launch {
+ val (conversationId, clientMessageId) = getMessageId() ?: return@launch
+ val result = feature(AutoMarkAsRead::class).markSnapAsSeen(conversationId, clientMessageId.toLong())
+ if (result == "DUPLICATEREQUEST") return@launch
+ if (result == null) {
+ if (config.messaging.skipWhenMarkingAsSeen.get()) {
+ withContext(Dispatchers.Main) {
+ parent.iterateParent {
+ it.triggerCloseTouchEvent()
+ false
+ }
+ }
+ }
+ inAppOverlay.showStatusToast(
+ Icons.Default.Info,
+ translation["mark_as_seen.seen_toast"],
+ durationMs = 800
+ )
+ } else {
+ inAppOverlay.showStatusToast(
+ Icons.Default.Info,
+ "Failed to mark as seen: $result",
+ )
+ }
+ }
+ }
+ }
+ }, 0)
+ }
+ }
+}+
\ No newline at end of file