From 2676e61d698db1ac490f8c374bf3fca221308e7b Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Fri, 21 May 2021 20:00:01 +0530 Subject: [PATCH] Embed avatar and chat images into HTML --- src/utils/exportUtils.js | 198 +++++++++++++++++++++++++++------------ 1 file changed, 137 insertions(+), 61 deletions(-) diff --git a/src/utils/exportUtils.js b/src/utils/exportUtils.js index e357ac6575..433600eac4 100644 --- a/src/utils/exportUtils.js +++ b/src/utils/exportUtils.js @@ -4,6 +4,8 @@ import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import JSZip from "jszip"; import { textForEvent } from "../TextForEvent"; import streamSaver from "streamsaver"; +import { decryptFile } from "./DecryptFile"; +import { mediaFromContent, mediaFromMxc } from "../customisations/Media"; const wrapHTML = (content, room) => (` @@ -33,36 +35,6 @@ const wrapHTML = (content, room) => (` `); -const getTimelineConversation = (room) => { - if (!room) return; - - const cli = MatrixClientPeg.get(); - - const timelineSet = room.getUnfilteredTimelineSet(); - - const timelineWindow = new TimelineWindow( - cli, timelineSet, - {windowLimit: Number.MAX_VALUE}); - - timelineWindow.load(null, 20); - - const events = timelineWindow.getEvents(); - - // Clone and reverse the events so that we preserve the order - arrayFastClone(events) - .reverse() - .forEach(event => { - cli.decryptEventIfNeeded(event); - }); - - if (!timelineWindow.canPaginate('f')) { - events.push(...timelineSet.getPendingEvents()); - } - console.log(events); - return events; -}; - - const css = ` body { margin: 0; @@ -297,6 +269,37 @@ div.selected { } `; + +const getTimelineConversation = (room) => { + if (!room) return; + + const cli = MatrixClientPeg.get(); + + const timelineSet = room.getUnfilteredTimelineSet(); + + const timelineWindow = new TimelineWindow( + cli, timelineSet, + {windowLimit: Number.MAX_VALUE}); + + timelineWindow.load(null, 20); + + const events = timelineWindow.getEvents(); + + // Clone and reverse the events so that we preserve the order + arrayFastClone(events) + .reverse() + .forEach(event => { + cli.decryptEventIfNeeded(event); + }); + + if (!timelineWindow.canPaginate('f')) { + events.push(...timelineSet.getPendingEvents()); + } + console.log(events); + return events; +}; + + const userColors = [ "#64bf47", "#4f9cd9", @@ -307,41 +310,50 @@ const userColors = [ ]; -//Get a color associated with a string. This is to map userId to a specific color +//Get a color associated with string length. This is to map userId to a specific color const getUserColor = (userId) => { return userColors[userId.length % 4]; }; -const createMessageBody = (event, joined = false, isReply = false, replyId = null) => { - return ` -
- ${!joined ? `
-
-
${event.sender.name[0]}
-
-
` : ``} -
-
${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}
- ${!joined ? `
- ${event.sender.name} -
`: ``} - ${isReply ? - `
- In reply to this message -
`: ``} -
${event.getContent().body}
-
-
- `; + +const getUserPic = async (event) => { + const member = event.sender; + if (!member.getMxcAvatarUrl()) { + return ` +
+
+
${event.sender.name[0]}
+
+
+ `; + } else { + const imageUrl = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(42, 42, "crop"); + + if (!avatars.has(member.userId)) { + avatars.set(member.userId, true); + const image = await fetch(imageUrl); + const blob = await image.blob(); + zip.file(`users/${member.userId}`, blob); + } + + return ` +
+
+ +
+
+ `; + } }; - +//Gets the event_id of an event to which an event is replied const baseEventId = (event) => { const isEncrypted = event.isEncrypted(); + // If encrypted, in_reply_to lies in event.event.content const content = isEncrypted ? event.event.content : event.getContent(); const relatesTo = content["m.relates_to"]; - return relatesTo ? relatesTo["m.in_reply_to"].event_id : null; + return (relatesTo && relatesTo["m.in_reply_to"]) ? relatesTo["m.in_reply_to"]["event_id"] : null; }; @@ -361,16 +373,75 @@ const dateSeparator = (event, prevEvent) => { return ""; }; -const createHTML = (events, room) => { +const getImageData = async (event) => { + let blob; + try { + const isEncrypted = event.isEncrypted(); + const content = event.getContent(); + if (isEncrypted) { + blob = await decryptFile(content.file); + } else { + const media = mediaFromContent(event.getContent()); + const image = await fetch(media.srcHttp); + blob = image.blob(); + } + } catch (err) { + console.log("Error decrypting image"); + } + return blob; +}; + + +const createMessageBody = async (event, joined = false, isReply = false, replyId = null) => { + const userPic = await getUserPic(event); + let messageBody = ""; + switch (event.getContent().msgtype) { + case "m.text": + messageBody = `
${event.getContent().body}
`; + break; + case "m.image": { + messageBody = ` + + `; + const blob = await getImageData(event); + zip.file(`images/${event.getId()}.png`, blob); + } + break; + default: + break; + } + + return ` +
+ ${!joined ? userPic : ``} +
+
${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}
+ ${!joined ? `
+ ${event.sender.name} +
`: ``} + ${isReply ? + `
+ In reply to this message +
`: ``} + ${messageBody} +
+
+ `; +}; + + +const createHTML = async (events, room) => { let content = ""; let prevEvent = null; for (const event of events) { content += dateSeparator(event, prevEvent); + if (event.getType() === "m.room.message") { const replyTo = baseEventId(event); const shouldBeJoined = prevEvent && prevEvent.getContent().msgtype === "m.text" && event.sender.userId === prevEvent.sender.userId && !dateSeparator(event, prevEvent) && !replyTo; - const body = createMessageBody(event, shouldBeJoined, !!replyTo, replyTo); + + const body = await createMessageBody(event, shouldBeJoined, !!replyTo, replyTo); content += body; } else { content += ` @@ -387,22 +458,27 @@ const createHTML = (events, room) => { }; +const avatars = new Map(); +let zip; const exportConversationalHistory = async (room) => { const res = getTimelineConversation(room); - const zip = new JSZip(); - const html = createHTML(res, room); - zip.file("css/style.css", css); + zip = new JSZip(); + + const html = await createHTML(res, room, avatars); + zip.file("index.html", html); + zip.file("css/style.css", css); + + avatars.clear(); const filename = `matrix-export-${new Date().toISOString()}.zip`; //Generate the zip file asynchronously const blob = await zip.generateAsync({ type: "blob" }); + //Create a writable stream to the directory const fileStream = streamSaver.createWriteStream(filename, blob.size); const writer = fileStream.getWriter(); - // console.log(blob.size); - // Here we chunk the blob into pieces of 10 MiB const sliceSize = 10 * 1e6; for (let fPointer = 0; fPointer < blob.size; fPointer += sliceSize) {