feat(core): mark as seen button

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk
2024-06-12 16:04:27 +02:00
parent 3f02023580
commit 248100b8b9
6 changed files with 161 additions and 66 deletions

View File

@@ -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"

View File

@@ -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()}

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}