mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 12:30:12 +02:00
refactor: better notifications
- caption in notifications - media preview - stack media messages - fix typing notification
This commit is contained in:
parent
97aed78894
commit
645b7befa9
@ -600,7 +600,49 @@
|
||||
},
|
||||
"better_notifications": {
|
||||
"name": "Better Notifications",
|
||||
"description": "Adds more information in received notifications"
|
||||
"description": "Adds more information in received notifications",
|
||||
"properties": {
|
||||
"group_notifications": {
|
||||
"name": "Group Notifications",
|
||||
"description": "Group notifications into a single one"
|
||||
},
|
||||
"chat_preview": {
|
||||
"name": "Chat Preview",
|
||||
"description": "Shows a preview of received messages in the notification"
|
||||
},
|
||||
"media_preview": {
|
||||
"name": "Media Preview",
|
||||
"description": "Shows a preview of the selected media types in the notification"
|
||||
},
|
||||
"media_caption": {
|
||||
"name": "Media Caption",
|
||||
"description": "Shows the attached caption of media in the notification"
|
||||
},
|
||||
"stacked_media_messages": {
|
||||
"name": "Stacked Media Messages",
|
||||
"description": "Combines multiple media messages into one text notification when they cannot be previewed. Use in combination with Chat Preview"
|
||||
},
|
||||
"friend_add_source": {
|
||||
"name": "Friend Add Source",
|
||||
"description": "Shows the source of a friend request in the notification"
|
||||
},
|
||||
"reply_button": {
|
||||
"name": "Reply Button",
|
||||
"description": "Adds a reply button to the notification"
|
||||
},
|
||||
"download_button": {
|
||||
"name": "Download Button",
|
||||
"description": "Allows you to download media from the notification"
|
||||
},
|
||||
"mark_as_read_button": {
|
||||
"name": "Mark as Read Button",
|
||||
"description": "Allows you to mark a message as read from the notification"
|
||||
},
|
||||
"mark_as_read_and_save_in_chat": {
|
||||
"name": "Mark as Read and Save in Chat",
|
||||
"description": "Adds a mark as read and save in chat button to the notification"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_blacklist": {
|
||||
"name": "Notification Blacklist",
|
||||
@ -1048,16 +1090,6 @@
|
||||
"always_light": "Always Light",
|
||||
"always_dark": "Always Dark"
|
||||
},
|
||||
"better_notifications": {
|
||||
"chat_preview": "Show a preview of chat",
|
||||
"media_preview": "Show a preview of media",
|
||||
"reply_button": "Add reply button",
|
||||
"download_button": "Add download button",
|
||||
"mark_as_read_button": "Mark as Read button",
|
||||
"mark_as_read_and_save_in_chat": "Save in Chat when marking as read (depends on Auto Save)",
|
||||
"friend_add_source": "Show friend add source",
|
||||
"group": "Group notifications"
|
||||
},
|
||||
"theme_picker": {
|
||||
"amoled_dark_mode": "AMOLED Dark Mode",
|
||||
"custom": "Custom Colors",
|
||||
|
@ -48,6 +48,21 @@ class MessagingTweaks : ConfigContainer() {
|
||||
}
|
||||
}
|
||||
|
||||
class BetterNotifications: ConfigContainer() {
|
||||
val groupNotifications = boolean("group_notifications")
|
||||
val chatPreview = boolean("chat_preview")
|
||||
val mediaPreview = multiple("media_preview", "SNAP", "NOTE", "EXTERNAL_MEDIA", "STICKER") {
|
||||
customOptionTranslationPath = "content_type"
|
||||
}
|
||||
val mediaCaption = boolean("media_caption")
|
||||
val stackedMediaMessages = boolean("stacked_media_messages")
|
||||
val friendAddSource = boolean("friend_add_source")
|
||||
val replyButton = boolean("reply_button") { addNotices(FeatureNotice.UNSTABLE) }
|
||||
val downloadButton = boolean("download_button")
|
||||
val markAsReadButton = boolean("mark_as_read_button") { addNotices(FeatureNotice.UNSTABLE) }
|
||||
val markAsReadAndSaveInChat = boolean("mark_as_read_and_save_in_chat") { addNotices(FeatureNotice.UNSTABLE) }
|
||||
}
|
||||
|
||||
val bypassScreenshotDetection = boolean("bypass_screenshot_detection") { requireRestart() }
|
||||
val anonymousStoryViewing = boolean("anonymous_story_viewing")
|
||||
val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() }
|
||||
@ -81,16 +96,7 @@ class MessagingTweaks : ConfigContainer() {
|
||||
"bitmoji_background_changes",
|
||||
"bitmoji_scene_changes",
|
||||
) { requireRestart() }
|
||||
val betterNotifications = multiple("better_notifications",
|
||||
"chat_preview",
|
||||
"media_preview",
|
||||
"reply_button",
|
||||
"download_button",
|
||||
"mark_as_read_button",
|
||||
"mark_as_read_and_save_in_chat",
|
||||
"friend_add_source",
|
||||
"group"
|
||||
) { requireRestart() }
|
||||
val betterNotifications = container("better_notifications", BetterNotifications()) { requireRestart() }
|
||||
val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) {
|
||||
customOptionTranslationPath = "features.options.notifications"
|
||||
}
|
||||
|
@ -574,7 +574,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
var previewBitmap: Bitmap? = null
|
||||
val previewCoroutine = context.coroutineScope.launch {
|
||||
runCatching {
|
||||
attachment.openStream { attachmentStream ->
|
||||
attachment.openStream { attachmentStream, _ ->
|
||||
val downloadedMediaList = mutableMapOf<SplitMediaAssetType, ByteArray>()
|
||||
|
||||
MediaDownloaderHelper.getSplitElements(attachmentStream!!) {
|
||||
|
@ -31,18 +31,21 @@ data class DecodedAttachment(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
inline fun openStream(crossinline callback: (InputStream?) -> Unit) {
|
||||
inline fun openStream(crossinline callback: (mediaStream: InputStream?, length: Long) -> Unit) {
|
||||
boltKey?.let { mediaUrlKey ->
|
||||
RemoteMediaResolver.downloadBoltMedia(Base64.decode(mediaUrlKey), decryptionCallback = {
|
||||
RemoteMediaResolver.downloadBoltMedia(Base64.UrlSafe.decode(mediaUrlKey), decryptionCallback = {
|
||||
attachmentInfo?.encryption?.decryptInputStream(it) ?: it
|
||||
}, resultCallback = { inputStream, _ ->
|
||||
callback(inputStream)
|
||||
}, resultCallback = { inputStream, length ->
|
||||
callback(inputStream, length)
|
||||
})
|
||||
} ?: directUrl?.let { rawMediaUrl ->
|
||||
URL(rawMediaUrl).openStream().let { inputStream ->
|
||||
attachmentInfo?.encryption?.decryptInputStream(inputStream) ?: inputStream
|
||||
}.use(callback)
|
||||
} ?: callback(null)
|
||||
val connection = URL(rawMediaUrl).openConnection()
|
||||
connection.getInputStream().let {
|
||||
attachmentInfo?.encryption?.decryptInputStream(it) ?: it
|
||||
}.use {
|
||||
callback(it, connection.contentLengthLong)
|
||||
}
|
||||
} ?: callback(null, 0)
|
||||
}
|
||||
|
||||
fun createInputMedia(
|
||||
|
@ -14,13 +14,11 @@ import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import kotlinx.coroutines.*
|
||||
import me.rhunk.snapenhance.common.data.ContentType
|
||||
import me.rhunk.snapenhance.common.data.MediaReferenceType
|
||||
import me.rhunk.snapenhance.common.data.FileType
|
||||
import me.rhunk.snapenhance.common.data.MessageUpdate
|
||||
import me.rhunk.snapenhance.common.data.NotificationType
|
||||
import me.rhunk.snapenhance.common.data.download.SplitMediaAssetType
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||
import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver
|
||||
import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
@ -81,10 +79,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
|
||||
private val translations by lazy { context.translation.getCategory("better_notifications") }
|
||||
|
||||
private val betterNotificationFilter by lazy {
|
||||
context.config.messaging.betterNotifications.get()
|
||||
}
|
||||
private val config by lazy { context.config.messaging.betterNotifications }
|
||||
|
||||
private fun newNotificationBuilder(notification: Notification) = XposedHelpers.newInstance(
|
||||
Notification.Builder::class.java,
|
||||
@ -138,7 +133,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
|
||||
newAction(translations["button.reply"], ACTION_REPLY, {
|
||||
betterNotificationFilter.contains("reply_button") && contentType == ContentType.CHAT
|
||||
config.replyButton.get() && contentType == ContentType.CHAT
|
||||
}) {
|
||||
val chatReplyInput = RemoteInput.Builder("chat_reply_input")
|
||||
.setLabel(translations["button.reply"])
|
||||
@ -147,11 +142,11 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
|
||||
newAction(translations["button.download"], ACTION_DOWNLOAD, {
|
||||
betterNotificationFilter.contains("download_button") && betterNotificationFilter.contains("media_preview") && (contentType == ContentType.EXTERNAL_MEDIA || contentType == ContentType.SNAP)
|
||||
config.downloadButton.get() && config.mediaPreview.get().contains(contentType.name)
|
||||
}) {}
|
||||
|
||||
newAction(translations["button.mark_as_read"], ACTION_MARK_AS_READ, {
|
||||
betterNotificationFilter.contains("mark_as_read_button")
|
||||
config.markAsReadButton.get()
|
||||
}) {}
|
||||
|
||||
val notificationBuilder = newNotificationBuilder(notificationData.notification).apply {
|
||||
@ -232,7 +227,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
)
|
||||
|
||||
if (betterNotificationFilter.contains("mark_as_read_and_save_in_chat")) {
|
||||
if (config.markAsReadAndSaveInChat.get()) {
|
||||
val messaging = context.feature(Messaging::class)
|
||||
val autoSave = context.feature(AutoSave::class)
|
||||
|
||||
@ -285,7 +280,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
val notificationId = if (forceCreate) System.nanoTime().toInt() else message.messageDescriptor?.conversationId?.toBytes().contentHashCode()
|
||||
sentNotifications.computeIfAbsent(notificationId) { conversationId }
|
||||
|
||||
if (betterNotificationFilter.contains("group")) {
|
||||
if (config.groupNotifications.get()) {
|
||||
runCatching {
|
||||
if (notificationManager.activeNotifications.firstOrNull {
|
||||
it.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0
|
||||
@ -336,41 +331,23 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}[orderKey] = if (includeUsername) "$senderUsername: $text" else text
|
||||
}
|
||||
|
||||
when (
|
||||
contentType.takeIf {
|
||||
(it != ContentType.SNAP && it != ContentType.EXTERNAL_MEDIA) || betterNotificationFilter.contains("media_preview")
|
||||
} ?: ContentType.UNKNOWN
|
||||
) {
|
||||
ContentType.CHAT -> {
|
||||
ProtoReader(message.messageContent!!.content!!).getString(2, 1)?.trim()?.let {
|
||||
setNotificationText(it)
|
||||
}
|
||||
computeMessages()
|
||||
}
|
||||
ContentType.SNAP, ContentType.EXTERNAL_MEDIA -> {
|
||||
val mediaReferences = MessageDecoder.getMediaReferences(
|
||||
messageContent = context.gson.toJsonTree(message.messageContent!!.instanceNonNull())
|
||||
)
|
||||
|
||||
val mediaReferenceKeys = mediaReferences.map { reference ->
|
||||
reference.asJsonObject.getAsJsonArray("mContentObject").map { it.asByte }.toByteArray()
|
||||
}
|
||||
|
||||
MessageDecoder.decode(message.messageContent!!).firstOrNull()?.also { media ->
|
||||
val mediaType = MediaReferenceType.valueOf(mediaReferences.first().asJsonObject["mMediaType"].asString)
|
||||
|
||||
runCatching {
|
||||
val downloadedMedia = RemoteMediaResolver.downloadBoltMedia(mediaReferenceKeys.first(), decryptionCallback = {
|
||||
media.attachmentInfo?.encryption?.decryptInputStream(it) ?: it
|
||||
}) ?: throw Throwable("Unable to download media")
|
||||
|
||||
if (config.mediaPreview.get().contains(contentType.name)) {
|
||||
MessageDecoder.decode(message.messageContent!!).firstOrNull()?.also { media ->
|
||||
runCatching {
|
||||
media.openStream { mediaStream, length ->
|
||||
if (mediaStream == null || length > 25 * 1024 * 1024) {
|
||||
context.log.error("Failed to open media stream or media is too large")
|
||||
sendNotification(message, data, true)
|
||||
return@openStream
|
||||
}
|
||||
val downloadedMedias = mutableMapOf<SplitMediaAssetType, ByteArray>()
|
||||
|
||||
MediaDownloaderHelper.getSplitElements(downloadedMedia.inputStream()) { type, inputStream ->
|
||||
MediaDownloaderHelper.getSplitElements(mediaStream) { type, inputStream ->
|
||||
downloadedMedias[type] = inputStream.readBytes()
|
||||
}
|
||||
|
||||
var bitmapPreview = PreviewUtils.createPreview(downloadedMedias[SplitMediaAssetType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!!
|
||||
val originalMedia = downloadedMedias[SplitMediaAssetType.ORIGINAL]!!
|
||||
var bitmapPreview = PreviewUtils.createPreview(originalMedia, FileType.fromByteArray(originalMedia).isVideo)!!
|
||||
|
||||
downloadedMedias[SplitMediaAssetType.OVERLAY]?.let {
|
||||
bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size))
|
||||
@ -381,19 +358,43 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?)
|
||||
}
|
||||
|
||||
if (config.mediaCaption.get()) {
|
||||
message.serialize()?.let {
|
||||
notificationBuilder.setContentText(it)
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(message, data.copy(notification = notificationBuilder.build()), true)
|
||||
return
|
||||
}.onFailure {
|
||||
context.log.error("Failed to send preview notification", it)
|
||||
}
|
||||
return
|
||||
}.onFailure {
|
||||
context.log.error("Failed to send preview notification", it)
|
||||
sendNotification(message, data, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
setNotificationText("[" + context.translation.getCategory("content_type")[contentType.name] + "]")
|
||||
computeMessages()
|
||||
}
|
||||
}
|
||||
if (!betterNotificationFilter.contains("chat_preview")) return
|
||||
|
||||
if (config.chatPreview.get()) {
|
||||
if (contentType == ContentType.CHAT) {
|
||||
setNotificationText(message.serialize() ?: "[Failed to parse message]")
|
||||
} else {
|
||||
if (config.stackedMediaMessages.get()) {
|
||||
setNotificationText(buildString {
|
||||
append("[")
|
||||
append(context.translation.getCategory("content_type")[contentType.name])
|
||||
append("]")
|
||||
if (config.mediaCaption.get()) {
|
||||
message.serialize()?.let { append(" $it") }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
sendNotification(message, data, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
computeMessages()
|
||||
}
|
||||
|
||||
sendNotification(message, data, false)
|
||||
}
|
||||
@ -419,7 +420,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3))
|
||||
val extras = notificationData.notification.extras.getBundle("system_notification_extras")?: return@hook
|
||||
|
||||
if (betterNotificationFilter.contains("group")) {
|
||||
if (config.groupNotifications.get()) {
|
||||
notificationData.notification.setObjectField("mGroupKey", SNAPCHAT_NOTIFICATION_GROUP)
|
||||
}
|
||||
|
||||
@ -430,7 +431,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
return@hook
|
||||
}
|
||||
|
||||
if (notificationType == "addfriend" && betterNotificationFilter.contains("friend_add_source")) {
|
||||
if (notificationType == "addfriend" && config.friendAddSource.get()) {
|
||||
val userId = notificationData.notification.shortcutId?.split("|")?.lastOrNull() ?: return@hook
|
||||
runBlocking {
|
||||
var addSource: String? = null
|
||||
@ -446,8 +447,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
return@hook
|
||||
}
|
||||
|
||||
if (!betterNotificationFilter.contains("chat_preview") && !betterNotificationFilter.contains("media_preview")) return@hook
|
||||
if (notificationType == "typing") return@hook
|
||||
if (!config.chatPreview.get() && config.mediaPreview.isEmpty()) return@hook
|
||||
if (notificationType.endsWith("typing")) return@hook
|
||||
|
||||
val serverMessageId = extras.getString("message_id") ?: return@hook
|
||||
val conversationId = extras.getString("conversation_id").also { id ->
|
||||
|
@ -114,7 +114,7 @@ class ConversationExporter(
|
||||
for (i in 0..5) {
|
||||
printLog("downloading ${attachment.boltKey ?: attachment.directUrl}... (attempt ${i + 1}/5)")
|
||||
runCatching {
|
||||
attachment.openStream { downloadedInputStream ->
|
||||
attachment.openStream { downloadedInputStream, _ ->
|
||||
MediaDownloaderHelper.getSplitElements(downloadedInputStream!!) { type, splitInputStream ->
|
||||
val mediaKey = "${type}_${attachment.mediaUniqueId}"
|
||||
val bufferedInputStream = BufferedInputStream(splitInputStream)
|
||||
|
Loading…
x
Reference in New Issue
Block a user