feat(core/message_exporter): new template design

This commit is contained in:
rhunk 2023-10-19 22:01:39 +02:00
parent 3bde12a2af
commit 5ac93fee3d
2 changed files with 327 additions and 254 deletions

View File

@ -1,166 +1,234 @@
<style> <style>
:root { :root {
--sigIconPrimary: #dedede; --Snap-sigIconPrimary: #dedede;
--sigIconSecondary: #999; --Snap-sigIconSecondary: #999;
--sigIconTertiary: #616161; --Snap-sigIconTertiary: #616161;
--sigIconNegative: #f23c57; --Snap-sigIconNegative: #f23c57;
--sigTextPrimary: #dedede; --Snap-sigTextPrimary: #dedede;
--sigTextPrimaryInverse: #000; --Snap-sigTextPrimaryInverse: #000;
--sigTextSecondary: #999; --Snap-sigTextSecondary: #999;
--sigTextTertiary: #616161; --Snap-sigTextTertiary: #616161;
--sigTextPlayer: #fff; --Snap-sigTextPlayer: #fff;
--sigTextNegative: #f23c57; --Snap-sigTextNegative: #f23c57;
--sigColorBackgroundBorder: rgba(255, 255, 255, 0.1); --Snap-sigColorBackgroundBorder: rgba(255, 255, 255, 0.1);
--sigBackgroundPrimary: #121212; --Snap-sigBackgroundPrimary: #121212;
--sigBackgroundPrimaryInverse: #fff; --Snap-sigBackgroundPrimaryInverse: #fff;
--sigBackgroundSecondary: #1e1e1e; --Snap-sigBackgroundSecondary: #1e1e1e;
--sigBackgroundSecondaryHover: #2b2b2b; --Snap-sigBackgroundSecondaryHover: #2b2b2b;
--sigBackgroundFeedHover: rgba(255, 255, 255, 0.1); --Snap-sigBackgroundFeedHover: rgba(255, 255, 255, 0.1);
--sigBackgroundMessageHover: #292929; --Snap-sigBackgroundMessageHover: #292929;
--sigBackgroundMessageSaved: #333232; --Snap-sigBackgroundMessageSaved: #333232;
--sigBackgroundMessageSavedHover: #3a3a3a; --Snap-sigBackgroundMessageSavedHover: #3a3a3a;
--sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1); --Snap-sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1);
--sigStartupFooterBackground: rgba(0, 0, 0, 0.05); --Snap-sigStartupFooterBackground: rgba(0, 0, 0, 0.05);
--sigButtonPrimary: #0fadff; --Snap-sigButtonPrimary: #0fadff;
--sigButtonPrimaryHover: #42bfff; --Snap-sigButtonPrimaryHover: #42bfff;
--sigButtonSecondary: #2b2b2b; --Snap-sigButtonSecondary: #2b2b2b;
--sigButtonSecondaryHover: #424242; --Snap-sigButtonSecondaryHover: #424242;
--sigButtonSecondaryActive: #5c5c5c; --Snap-sigButtonSecondaryActive: #5c5c5c;
--sigButtonTertiary: #4e565f; --Snap-sigButtonTertiary: #4e565f;
--sigButtonQuaternary: #fff; --Snap-sigButtonQuaternary: #fff;
--sigButtonInactive: #1e1e1e; --Snap-sigButtonInactive: #1e1e1e;
--sigButtonNegative: #e1143d; --Snap-sigButtonNegative: #e1143d;
--sigButtonOnPrimary: #fff; --Snap-sigButtonOnPrimary: #fff;
--sigButtonOnSecondary: #dedede; --Snap-sigButtonOnSecondary: #dedede;
--sigButtonOnTertiary: #fff; --Snap-sigButtonOnTertiary: #fff;
--sigButtonOnQuaternary: #1e1e1e; --Snap-sigButtonOnQuaternary: #1e1e1e;
--sigButtonOnInactive: rgba(255, 255, 255, 0.3); --Snap-sigButtonOnInactive: rgba(255, 255, 255, 0.3);
--sigButtonOnNegative: #fff; --Snap-sigButtonOnNegative: #fff;
--sigMain: #121212; --Snap-sigMain: #121212;
--sigSubscreen: #121212; --Snap-sigSubscreen: #121212;
--sigOverlay: rgba(0, 0, 0, 0.4); --Snap-sigOverlay: rgba(0, 0, 0, 0.4);
--sigOverlayHover: rgba(0, 0, 0, 0.35); --Snap-sigOverlayHover: rgba(0, 0, 0, 0.35);
--sigSurface: #1e1e1e; --Snap-sigSurface: #1e1e1e;
--sigSurfaceRGB: 30, 30, 30; --Snap-sigSurfaceRGB: 30, 30, 30;
--sigSurfaceDown: #212121; --Snap-sigSurfaceDown: #212121;
--sigAboveSurface: #292929; --Snap-sigAboveSurface: #292929;
--sigObject: rgba(255, 255, 255, 0.1); --Snap-sigObject: rgba(255, 255, 255, 0.1);
--sigObjectDown: rgba(255, 255, 255, 0.19); --Snap-sigObjectDown: rgba(255, 255, 255, 0.19);
--sigConversationBoxBackground: rgba(255, 255, 255, 0.25); --Snap-sigConversationBoxBackground: rgba(255, 255, 255, 0.25);
--sigDivider: rgba(255, 255, 255, 0.1); --Snap-sigDivider: rgba(255, 255, 255, 0.1);
--sigDividerLight: rgba(255, 255, 255, 0.2); --Snap-sigDividerLight: rgba(255, 255, 255, 0.2);
--sigPlaceholder: #1e1e1e; --Snap-sigPlaceholder: #1e1e1e;
--sigDisabled: rgba(255, 255, 255, 0.1); --Snap-sigDisabled: rgba(255, 255, 255, 0.1);
--sigCallTileHighlight: rgba(255, 255, 255, 0.8); --Snap-sigCallTileHighlight: rgba(255, 255, 255, 0.8);
--sigChat: #0fadff; --Snap-sigChat: #0fadff;
--sigSnapWithoutSound: #f23c57; --Snap-sigSnapWithoutSound: #f23c57;
--sigSnapWithSound: #a05dcd; --Snap-sigSnapWithSound: #a05dcd;
--sigChatSurfaceCalling: #39ca8e; --Snap-sigChatSurfaceCalling: #39ca8e;
--sigChatSurfaceCallingDisabled: #105e3d; --Snap-sigChatSurfaceCallingDisabled: #105e3d;
--sigChatPending: #767676; --Snap-sigChatPending: #767676;
--sigChatPendingHover: #8f8f8f; --Snap-sigChatPendingHover: #8f8f8f;
--sigChatIcon: #0fadff; --Snap-sigChatIcon: #0fadff;
--sigChatIconCaret: #f8616d; --Snap-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); --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);
--selectedMiddleColorGradient: rgba(4, 4, 4, 0.1); --Snap-selectedMiddleColorGradient: rgba(4, 4, 4, 0.1);
--selectedRightColorGradient: rgba(4, 4, 4, 0); --Snap-selectedRightColorGradient: rgba(4, 4, 4, 0);
--Border: 1px solid var(--Snap-sigColorBackgroundBorder);
} }
body { body {
margin: 0;
font-family: 'Avenir Next', sans-serif; font-family: 'Avenir Next', sans-serif;
color: var(--sigTextPrimary); color: var(--Snap-sigTextPrimary);
background-color: var(--sigBackgroundPrimary); background-color: var(--Snap-sigBackgroundPrimary);
} margin: 0;
padding: 0;
/* like an header */
.conversation_summary {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
border-bottom: 1px solid var(--sigColorBackgroundBorder);
}
.conversation_message_container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 5px;
background-color: var(--sigBackgroundSecondary);
}
.message {
display: flex;
flex-direction: row;
align-items: center; align-items: center;
padding: 5px; justify-content: flex-start;
margin-left: 5px;
border-bottom: 1px solid var(--sigColorBackgroundBorder);
} }
.message .header { header {
width: 100%;
padding: 10px 0px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
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;
}
main {
background-color: var(--Snap-sigBackgroundSecondary);
border: var(--Border);
border-radius: 12px;
width: calc(100% - 30px);
display: flex;
flex-direction: column;
}
main>.message {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
flex-wrap: nowrap;
margin: 5px 15px;
}
main>.message .header {
width: 100%;
display: flex;
vertical-align: top; vertical-align: top;
align-self: flex-start; 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; font-weight: bold;
} }
.message .time { main>.message .time {
color: var(--Snap-sigTextSecondary);
font-size: 12px; font-size: 12px;
color: var(--sigTextSecondary); font-weight: 600;
} }
.message .content { main>.message:nth-child(2n) .content {
margin-left: 10px; border-color: #dcedc1;
display: flex;
flex-direction: row;
align-items: center;
} }
.chat_media { main>.message:nth-child(2n + 1) .content {
max-width: 300px; border-color: #ffd3b6;
max-height: 500px;
} }
.overlay_media { 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;
}
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; position: absolute;
pointer-events: none; pointer-events: none;
} }
.red_snap_svg { main>.message .red_snap_svg {
color: var(--sigSnapWithoutSound); color: var(--Snap-sigSnapWithoutSound);
} }
</style> </style>
<body> <body>
<div class="conversation_summary"> <header>
<div class="title"></div> <div class="title"></div>
</div> </header>
<div class="conversation_message_container"></div> <main></main>
<div style="display: none;">
<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"> <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> <rect x="4" y="5" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect>
</svg> </svg>
</div> </div>
<script>
const conversationData = JSON.parse(document.querySelector(".exported_content").innerHTML)
const participants = Object.values(conversationData.participants)
<script>
function base64decode(data) { function base64decode(data) {
return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0))) return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0)))
} }
function makeConversationSummary() { const conversationData = JSON.parse(new TextDecoder().decode(new Uint8Array(inflate(base64decode(document.querySelector(".exported_content").innerHTML)))))
const conversationTitle = conversationData.conversationName != null ? const participants = Object.values(conversationData.participants)
conversationData.conversationName : "DM with " + Object.values(participants).map(user => user.username).join(", ")
document.querySelector(".conversation_summary .title").textContent = conversationTitle 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) { function decodeMedia(element) {
@ -175,16 +243,16 @@
return URL.createObjectURL(new Blob([decodedData])) return URL.createObjectURL(new Blob([decodedData]))
} }
function makeConversationMessageContainer() { function makeMain() {
const messageTemplate = document.querySelector("#message_template") const messageTemplate = document.querySelector("#message_template")
Object.values(conversationData.messages).forEach(message => { Object.values(conversationData.messages).forEach(message => {
const messageObject = document.createElement("div") const messageObject = document.createElement("div")
messageObject.classList.add("message") messageObject.classList.add("message")
messageObject.appendChild(((headerElement) =>{ messageObject.appendChild(((headerElement) => {
headerElement.classList.add("header") headerElement.classList.add("header")
headerElement.appendChild(((elem) =>{ headerElement.appendChild(((elem) => {
elem.classList.add("username") elem.classList.add("username")
const participant = participants[message.senderId] const participant = participants[message.senderId]
elem.innerHTML = (participant == null) ? "Unknown user" : participant.username elem.innerHTML = (participant == null) ? "Unknown user" : participant.username
@ -192,24 +260,23 @@
})(document.createElement("div"))) })(document.createElement("div")))
headerElement.appendChild(((elem) =>{ headerElement.appendChild(((elem) => {
elem.classList.add("time") elem.classList.add("time")
elem.innerHTML = new Date(message.createdTimestamp).toISOString() elem.innerHTML = new Date(message.createdTimestamp).toUTCString()
return elem return elem
})(document.createElement("div"))) })(document.createElement("div")))
return headerElement return headerElement
})(document.createElement("div"))) })(document.createElement("div")))
messageObject.appendChild(((messageContainer) =>{ messageObject.appendChild(((messageContainer) => {
messageContainer.classList.add("content") messageContainer.classList.add("content")
messageContainer.innerHTML = message.serializedContent messageContainer.innerHTML = message.serializedContent
if (!message.serializedContent) { if (!message.serializedContent) {
messageContainer.innerHTML = "" messageContainer.innerHTML = ""
let messageData = "" let messageData = ""
switch(message.type) { switch (message.type) {
case "SNAP": case "SNAP":
messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true)) messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true))
messageData = "Snap" messageData = "Snap"
@ -268,7 +335,7 @@
let fetched = false let fetched = false
new IntersectionObserver(entries => { new IntersectionObserver(entries => {
if(!fetched && entries[0].isIntersecting === true) { if (!fetched && entries[0].isIntersecting === true) {
fetched = true fetched = true
messageContainer.innerHTML = "" messageContainer.innerHTML = ""
observers.forEach(c => { observers.forEach(c => {
@ -285,11 +352,11 @@
return messageContainer return messageContainer
})(document.createElement("div"))) })(document.createElement("div")))
document.querySelector('.conversation_message_container').appendChild(messageObject) document.querySelector('main').appendChild(messageObject)
}) })
} }
makeConversationSummary() makeHeader()
makeConversationMessageContainer() makeMain()
</script> </script>
</body> </body>

View File

@ -2,6 +2,7 @@ package me.rhunk.snapenhance.core.messaging
import android.os.Environment import android.os.Environment
import android.util.Base64InputStream import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject 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.features.impl.downloader.decoder.MessageDecoder
import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import java.io.BufferedInputStream import java.io.*
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Collections import java.util.Collections
import java.util.Date import java.util.Date
@ -34,6 +31,7 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.DeflaterInputStream import java.util.zip.DeflaterInputStream
import java.util.zip.DeflaterOutputStream
import java.util.zip.ZipFile import java.util.zip.ZipFile
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@ -172,17 +170,15 @@ class MessageExporter(
mediaFiles.forEach { (key, filePair) -> mediaFiles.forEach { (key, filePair) ->
output.write("<div class=\"media-$key\"><!-- ".toByteArray()) output.write("<div class=\"media-$key\"><!-- ".toByteArray())
filePair.second.inputStream().use { inputStream ->
val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) val deflateInputStream = DeflaterInputStream(inputStream, Deflater(Deflater.BEST_COMPRESSION, true))
val base64InputStream = XposedHelpers.newInstance( (XposedHelpers.newInstance(
Base64InputStream::class.java, Base64InputStream::class.java,
deflateInputStream, deflateInputStream,
android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
true true
) as InputStream ) as InputStream).copyTo(output)
base64InputStream.copyTo(output) }
deflateInputStream.close()
output.write(" --></div>\n".toByteArray()) output.write(" --></div>\n".toByteArray())
output.flush() output.flush()
updateProgress("wrote") updateProgress("wrote")
@ -191,7 +187,17 @@ class MessageExporter(
//write the json file //write the json file
output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) 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()) output.write("</script>\n".toByteArray())
printLog("writing template...") printLog("writing template...")