commit 5ac93fee3d332ebb82792968c9a6e96bc8c36f5f
parent 3bde12a2af8e4123ff41729b371808b7838999a7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 19 Oct 2023 22:01:39 +0200

feat(core/message_exporter): new template design

Diffstat:
Mcore/src/main/assets/web/export_template.html | 520+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageExporter.kt | 40+++++++++++++++++++++++-----------------
2 files changed, 317 insertions(+), 243 deletions(-)

diff --git a/core/src/main/assets/web/export_template.html b/core/src/main/assets/web/export_template.html @@ -1,295 +1,362 @@ <style> :root { - --sigIconPrimary: #dedede; - --sigIconSecondary: #999; - --sigIconTertiary: #616161; - --sigIconNegative: #f23c57; - --sigTextPrimary: #dedede; - --sigTextPrimaryInverse: #000; - --sigTextSecondary: #999; - --sigTextTertiary: #616161; - --sigTextPlayer: #fff; - --sigTextNegative: #f23c57; - --sigColorBackgroundBorder: rgba(255, 255, 255, 0.1); - --sigBackgroundPrimary: #121212; - --sigBackgroundPrimaryInverse: #fff; - --sigBackgroundSecondary: #1e1e1e; - --sigBackgroundSecondaryHover: #2b2b2b; - --sigBackgroundFeedHover: rgba(255, 255, 255, 0.1); - --sigBackgroundMessageHover: #292929; - --sigBackgroundMessageSaved: #333232; - --sigBackgroundMessageSavedHover: #3a3a3a; - --sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1); - --sigStartupFooterBackground: rgba(0, 0, 0, 0.05); - --sigButtonPrimary: #0fadff; - --sigButtonPrimaryHover: #42bfff; - --sigButtonSecondary: #2b2b2b; - --sigButtonSecondaryHover: #424242; - --sigButtonSecondaryActive: #5c5c5c; - --sigButtonTertiary: #4e565f; - --sigButtonQuaternary: #fff; - --sigButtonInactive: #1e1e1e; - --sigButtonNegative: #e1143d; - --sigButtonOnPrimary: #fff; - --sigButtonOnSecondary: #dedede; - --sigButtonOnTertiary: #fff; - --sigButtonOnQuaternary: #1e1e1e; - --sigButtonOnInactive: rgba(255, 255, 255, 0.3); - --sigButtonOnNegative: #fff; - --sigMain: #121212; - --sigSubscreen: #121212; - --sigOverlay: rgba(0, 0, 0, 0.4); - --sigOverlayHover: rgba(0, 0, 0, 0.35); - --sigSurface: #1e1e1e; - --sigSurfaceRGB: 30, 30, 30; - --sigSurfaceDown: #212121; - --sigAboveSurface: #292929; - --sigObject: rgba(255, 255, 255, 0.1); - --sigObjectDown: rgba(255, 255, 255, 0.19); - --sigConversationBoxBackground: rgba(255, 255, 255, 0.25); - --sigDivider: rgba(255, 255, 255, 0.1); - --sigDividerLight: rgba(255, 255, 255, 0.2); - --sigPlaceholder: #1e1e1e; - --sigDisabled: rgba(255, 255, 255, 0.1); - --sigCallTileHighlight: rgba(255, 255, 255, 0.8); - --sigChat: #0fadff; - --sigSnapWithoutSound: #f23c57; - --sigSnapWithSound: #a05dcd; - --sigChatSurfaceCalling: #39ca8e; - --sigChatSurfaceCallingDisabled: #105e3d; - --sigChatPending: #767676; - --sigChatPendingHover: #8f8f8f; - --sigChatIcon: #0fadff; - --sigChatIconCaret: #f8616d; - --sigChatShadowOne: 0 0 17px rgba(33, 33, 33, 0.07), 0 0 22px rgba(0, 0, 0, 0.06), 0 0 8px rgba(84, 84, 84, 0.1); - --selectedMiddleColorGradient: rgba(4, 4, 4, 0.1); - --selectedRightColorGradient: rgba(4, 4, 4, 0); + --Snap-sigIconPrimary: #dedede; + --Snap-sigIconSecondary: #999; + --Snap-sigIconTertiary: #616161; + --Snap-sigIconNegative: #f23c57; + --Snap-sigTextPrimary: #dedede; + --Snap-sigTextPrimaryInverse: #000; + --Snap-sigTextSecondary: #999; + --Snap-sigTextTertiary: #616161; + --Snap-sigTextPlayer: #fff; + --Snap-sigTextNegative: #f23c57; + --Snap-sigColorBackgroundBorder: rgba(255, 255, 255, 0.1); + --Snap-sigBackgroundPrimary: #121212; + --Snap-sigBackgroundPrimaryInverse: #fff; + --Snap-sigBackgroundSecondary: #1e1e1e; + --Snap-sigBackgroundSecondaryHover: #2b2b2b; + --Snap-sigBackgroundFeedHover: rgba(255, 255, 255, 0.1); + --Snap-sigBackgroundMessageHover: #292929; + --Snap-sigBackgroundMessageSaved: #333232; + --Snap-sigBackgroundMessageSavedHover: #3a3a3a; + --Snap-sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1); + --Snap-sigStartupFooterBackground: rgba(0, 0, 0, 0.05); + --Snap-sigButtonPrimary: #0fadff; + --Snap-sigButtonPrimaryHover: #42bfff; + --Snap-sigButtonSecondary: #2b2b2b; + --Snap-sigButtonSecondaryHover: #424242; + --Snap-sigButtonSecondaryActive: #5c5c5c; + --Snap-sigButtonTertiary: #4e565f; + --Snap-sigButtonQuaternary: #fff; + --Snap-sigButtonInactive: #1e1e1e; + --Snap-sigButtonNegative: #e1143d; + --Snap-sigButtonOnPrimary: #fff; + --Snap-sigButtonOnSecondary: #dedede; + --Snap-sigButtonOnTertiary: #fff; + --Snap-sigButtonOnQuaternary: #1e1e1e; + --Snap-sigButtonOnInactive: rgba(255, 255, 255, 0.3); + --Snap-sigButtonOnNegative: #fff; + --Snap-sigMain: #121212; + --Snap-sigSubscreen: #121212; + --Snap-sigOverlay: rgba(0, 0, 0, 0.4); + --Snap-sigOverlayHover: rgba(0, 0, 0, 0.35); + --Snap-sigSurface: #1e1e1e; + --Snap-sigSurfaceRGB: 30, 30, 30; + --Snap-sigSurfaceDown: #212121; + --Snap-sigAboveSurface: #292929; + --Snap-sigObject: rgba(255, 255, 255, 0.1); + --Snap-sigObjectDown: rgba(255, 255, 255, 0.19); + --Snap-sigConversationBoxBackground: rgba(255, 255, 255, 0.25); + --Snap-sigDivider: rgba(255, 255, 255, 0.1); + --Snap-sigDividerLight: rgba(255, 255, 255, 0.2); + --Snap-sigPlaceholder: #1e1e1e; + --Snap-sigDisabled: rgba(255, 255, 255, 0.1); + --Snap-sigCallTileHighlight: rgba(255, 255, 255, 0.8); + --Snap-sigChat: #0fadff; + --Snap-sigSnapWithoutSound: #f23c57; + --Snap-sigSnapWithSound: #a05dcd; + --Snap-sigChatSurfaceCalling: #39ca8e; + --Snap-sigChatSurfaceCallingDisabled: #105e3d; + --Snap-sigChatPending: #767676; + --Snap-sigChatPendingHover: #8f8f8f; + --Snap-sigChatIcon: #0fadff; + --Snap-sigChatIconCaret: #f8616d; + --Snap-sigChatShadowOne: 0 0 17px rgba(33, 33, 33, 0.07), 0 0 22px rgba(0, 0, 0, 0.06), 0 0 8px rgba(84, 84, 84, 0.1); + --Snap-selectedMiddleColorGradient: rgba(4, 4, 4, 0.1); + --Snap-selectedRightColorGradient: rgba(4, 4, 4, 0); + --Border: 1px solid var(--Snap-sigColorBackgroundBorder); } body { - margin: 0; font-family: 'Avenir Next', sans-serif; - color: var(--sigTextPrimary); - background-color: var(--sigBackgroundPrimary); + color: var(--Snap-sigTextPrimary); + background-color: var(--Snap-sigBackgroundPrimary); + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; } - /* like an header */ - .conversation_summary { + header { + width: 100%; + padding: 10px 0px; display: flex; - flex-direction: row; + flex-direction: column; align-items: center; - padding: 10px; - border-bottom: 1px solid var(--sigColorBackgroundBorder); + justify-content: flex-start; + } + + header .title { + background-color: var(--Snap-sigButtonSecondary); + height: 40px; + font-weight: 600; + padding-inline-end: 16px; + border-radius: 999px; + line-height: 40px; + padding: 0 20px; } - .conversation_message_container { + main { + background-color: var(--Snap-sigBackgroundSecondary); + border: var(--Border); + border-radius: 12px; + width: calc(100% - 30px); display: flex; flex-direction: column; - padding: 5px; - background-color: var(--sigBackgroundSecondary); } - .message { + main>.message { display: flex; - flex-direction: row; - align-items: center; - padding: 5px; - margin-left: 5px; - border-bottom: 1px solid var(--sigColorBackgroundBorder); + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + flex-wrap: nowrap; + margin: 5px 15px; } - .message .header { + main>.message .header { + width: 100%; display: flex; - flex-direction: column; vertical-align: top; align-self: flex-start; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; } - .message .username { + main>.message:nth-child(2n) .username { + color: #dcedc1; + } + + main>.message:nth-child(2n + 1) .username { + color: #ffd3b6; + } + + main>.message .username { font-weight: bold; } - .message .time { + main>.message .time { + color: var(--Snap-sigTextSecondary); font-size: 12px; - color: var(--sigTextSecondary); + font-weight: 600; } - .message .content { - margin-left: 10px; - display: flex; - flex-direction: row; - align-items: center; + main>.message:nth-child(2n) .content { + border-color: #dcedc1; + } + main>.message:nth-child(2n + 1) .content { + border-color: #ffd3b6; } - .chat_media { - max-width: 300px; - max-height: 500px; + main>.message .content { + background-color: var(--Snap-sigBackgroundMessageSaved); + border-left: 3px solid; + border-radius: 3px; + margin-top: 4px; + padding-left: 4px; + padding: 3px 0 3px 6px; } - .overlay_media { + main>.message .content div:has(.chat_media:not(audio):not(.overlay_media)) { + display: inline-block; + resize: horizontal; + overflow: hidden; + line-height: 0; + height: auto; + width: 300px; + } + + main>.message .chat_media:not(audio):not(.overlay_media) { + width: 100%; + height: auto; + } + + @-moz-document url-prefix() { + main>.message .content div { + display: inline-block; + resize: horizontal; + overflow: hidden; + line-height: 0; + height: auto; + width: 300px; + } + + main>.message .chat_media:not(.overlay_media) { + width: 100%; + height: auto; + } + } + + + main>.message .overlay_media { + width: inherit; + height: inherit; position: absolute; pointer-events: none; } - .red_snap_svg { - color: var(--sigSnapWithoutSound); + main>.message .red_snap_svg { + color: var(--Snap-sigSnapWithoutSound); } </style> <body> - <div class="conversation_summary"> - <div class="title"></div> - </div> - - <div class="conversation_message_container"></div> - <div style="display: none;"> - <svg class="red_snap_svg" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <rect x="2.75" y="2.75" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect> - </svg> - </div> - - <script> - const conversationData = JSON.parse(document.querySelector(".exported_content").innerHTML) - const participants = Object.values(conversationData.participants) - - function base64decode(data) { - return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0))) - } +<header> + <div class="title"></div> +</header> - function makeConversationSummary() { - const conversationTitle = conversationData.conversationName != null ? - conversationData.conversationName : "DM with " + Object.values(participants).map(user => user.username).join(", ") +<main></main> - document.querySelector(".conversation_summary .title").textContent = conversationTitle - } +<div style="display: none;"> + <svg class="red_snap_svg" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="4" y="5" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect> + </svg> +</div> - function decodeMedia(element) { - const decodedData = new Uint8Array( - inflate( - base64decode( - element.innerHTML.substring(5, element.innerHTML.length - 4) - ) +<script> + function base64decode(data) { + return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0))) + } + + const conversationData = JSON.parse(new TextDecoder().decode(new Uint8Array(inflate(base64decode(document.querySelector(".exported_content").innerHTML))))) + const participants = Object.values(conversationData.participants) + + function makeHeader() { + const conversationTitle = conversationData.conversationName != null ? conversationData.conversationName : "DM with " + Object.values(participants).map(user => user.username).join(", ") + document.querySelector("header > .title").textContent = conversationTitle + document.title = conversationTitle + } + + function decodeMedia(element) { + const decodedData = new Uint8Array( + inflate( + base64decode( + element.innerHTML.substring(5, element.innerHTML.length - 4) ) ) + ) - return URL.createObjectURL(new Blob([decodedData])) - } - - function makeConversationMessageContainer() { - const messageTemplate = document.querySelector("#message_template") - Object.values(conversationData.messages).forEach(message => { - const messageObject = document.createElement("div") - messageObject.classList.add("message") + return URL.createObjectURL(new Blob([decodedData])) + } - messageObject.appendChild(((headerElement) =>{ - headerElement.classList.add("header") + function makeMain() { + const messageTemplate = document.querySelector("#message_template") + Object.values(conversationData.messages).forEach(message => { + const messageObject = document.createElement("div") + messageObject.classList.add("message") - headerElement.appendChild(((elem) =>{ - elem.classList.add("username") - const participant = participants[message.senderId] - elem.innerHTML = (participant == null) ? "Unknown user" : participant.username - return elem - })(document.createElement("div"))) + messageObject.appendChild(((headerElement) => { + headerElement.classList.add("header") + headerElement.appendChild(((elem) => { + elem.classList.add("username") + const participant = participants[message.senderId] + elem.innerHTML = (participant == null) ? "Unknown user" : participant.username + return elem + })(document.createElement("div"))) - headerElement.appendChild(((elem) =>{ - elem.classList.add("time") - elem.innerHTML = new Date(message.createdTimestamp).toISOString() - return elem - })(document.createElement("div"))) - return headerElement + headerElement.appendChild(((elem) => { + elem.classList.add("time") + elem.innerHTML = new Date(message.createdTimestamp).toUTCString() + return elem })(document.createElement("div"))) - messageObject.appendChild(((messageContainer) =>{ - messageContainer.classList.add("content") - - messageContainer.innerHTML = message.serializedContent - - if (!message.serializedContent) { - messageContainer.innerHTML = "" - let messageData = "" - switch(message.type) { - case "SNAP": - messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true)) - messageData = "Snap" - break - default: - messageData = message.type - } - messageContainer.innerHTML += messageData + return headerElement + })(document.createElement("div"))) + + messageObject.appendChild(((messageContainer) => { + messageContainer.classList.add("content") + messageContainer.innerHTML = message.serializedContent + + if (!message.serializedContent) { + messageContainer.innerHTML = "" + let messageData = "" + switch (message.type) { + case "SNAP": + messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true)) + messageData = "Snap" + break + default: + messageData = message.type } + messageContainer.innerHTML += messageData + } - if (message.attachments && message.attachments.length > 0) { - let observers = [] + if (message.attachments && message.attachments.length > 0) { + let observers = [] - message.attachments.forEach((attachment, index) => { - const mediaKey = attachment.key.replace(/(=)/g, "") + message.attachments.forEach((attachment, index) => { + const mediaKey = attachment.key.replace(/(=)/g, "") - observers.push(() => { - const originalMedia = document.querySelector('.media-ORIGINAL_' + mediaKey) - if (!originalMedia) { - return - } + observers.push(() => { + const originalMedia = document.querySelector('.media-ORIGINAL_' + mediaKey) + if (!originalMedia) { + return + } - const originalMediaUrl = decodeMedia(originalMedia) + const originalMediaUrl = decodeMedia(originalMedia) - const mediaContainer = document.createElement("div") - messageContainer.appendChild(mediaContainer) + const mediaContainer = document.createElement("div") + messageContainer.appendChild(mediaContainer) - const imageTag = document.createElement("img") - imageTag.src = originalMediaUrl - imageTag.classList.add("chat_media") - mediaContainer.appendChild(imageTag) + const imageTag = document.createElement("img") + imageTag.src = originalMediaUrl + imageTag.classList.add("chat_media") + mediaContainer.appendChild(imageTag) - imageTag.onerror = () => { - mediaContainer.removeChild(imageTag) - const mediaTag = document.createElement(message.type === "NOTE" ? "audio" : "video") - mediaTag.classList.add("chat_media") - mediaTag.src = originalMediaUrl - mediaTag.preload = "metadata" - mediaTag.controls = true - mediaContainer.appendChild(mediaTag) - } + imageTag.onerror = () => { + mediaContainer.removeChild(imageTag) + const mediaTag = document.createElement(message.type === "NOTE" ? "audio" : "video") + mediaTag.classList.add("chat_media") + mediaTag.src = originalMediaUrl + mediaTag.preload = "metadata" + mediaTag.controls = true + mediaContainer.appendChild(mediaTag) + } - const overlay = document.querySelector('.media-OVERLAY_' + mediaKey) - if (!overlay) { - return - } + const overlay = document.querySelector('.media-OVERLAY_' + mediaKey) + if (!overlay) { + return + } - const overlayImage = document.createElement("img") - overlayImage.src = decodeMedia(overlay) - overlayImage.classList.add("chat_media") - overlayImage.classList.add("overlay_media") - mediaContainer.appendChild(overlayImage) - }) + const overlayImage = document.createElement("img") + overlayImage.src = decodeMedia(overlay) + overlayImage.classList.add("chat_media") + overlayImage.classList.add("overlay_media") + mediaContainer.appendChild(overlayImage) }) + }) + + let fetched = false + + new IntersectionObserver(entries => { + if (!fetched && entries[0].isIntersecting === true) { + fetched = true + messageContainer.innerHTML = "" + observers.forEach(c => { + try { + c() + } catch (e) { + console.log(e) + } + }) + } + }).observe(messageContainer) + } - let fetched = false - - new IntersectionObserver(entries => { - if(!fetched && entries[0].isIntersecting === true) { - fetched = true - messageContainer.innerHTML = "" - observers.forEach(c => { - try { - c() - } catch (e) { - console.log(e) - } - }) - } - }).observe(messageContainer) - } - - return messageContainer - })(document.createElement("div"))) + return messageContainer + })(document.createElement("div"))) - document.querySelector('.conversation_message_container').appendChild(messageObject) - }) - } + document.querySelector('main').appendChild(messageObject) + }) + } - makeConversationSummary() - makeConversationMessageContainer() - </script> -</body> + makeHeader() + makeMain() +</script> +</body>+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageExporter.kt @@ -2,6 +2,7 @@ package me.rhunk.snapenhance.core.messaging import android.os.Environment import android.util.Base64InputStream +import android.util.Base64OutputStream import com.google.gson.JsonArray import com.google.gson.JsonNull import com.google.gson.JsonObject @@ -21,11 +22,7 @@ import me.rhunk.snapenhance.core.features.impl.downloader.decoder.AttachmentType import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID -import java.io.BufferedInputStream -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream +import java.io.* import java.text.SimpleDateFormat import java.util.Collections import java.util.Date @@ -34,6 +31,7 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.zip.Deflater import java.util.zip.DeflaterInputStream +import java.util.zip.DeflaterOutputStream import java.util.zip.ZipFile import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -172,17 +170,15 @@ class MessageExporter( mediaFiles.forEach { (key, filePair) -> output.write("<div class=\"media-$key\"><!-- ".toByteArray()) - - val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) - val base64InputStream = XposedHelpers.newInstance( - Base64InputStream::class.java, - deflateInputStream, - android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, - true - ) as InputStream - base64InputStream.copyTo(output) - deflateInputStream.close() - + filePair.second.inputStream().use { inputStream -> + val deflateInputStream = DeflaterInputStream(inputStream, Deflater(Deflater.BEST_COMPRESSION, true)) + (XposedHelpers.newInstance( + Base64InputStream::class.java, + deflateInputStream, + android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, + true + ) as InputStream).copyTo(output) + } output.write(" --></div>\n".toByteArray()) output.flush() updateProgress("wrote") @@ -191,7 +187,17 @@ class MessageExporter( //write the json file output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) - exportJson(output) + + val deflateOutputStream = DeflaterOutputStream((XposedHelpers.newInstance( + Base64OutputStream::class.java, + output, + android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, + true + ) as OutputStream), Deflater(Deflater.BEST_COMPRESSION, true)) + + exportJson(deflateOutputStream) + deflateOutputStream.finish() + output.write("</script>\n".toByteArray()) printLog("writing template...")