Refactor, move HTML export to a new file and switch to TypeScript

This commit is contained in:
Jaiwanth 2021-05-22 11:32:55 +05:30
parent e59f23880b
commit dfb7aa4320
3 changed files with 98 additions and 69 deletions

View file

@ -32,7 +32,8 @@ import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import {PlaceCallType} from "../../../CallHandler"; import {PlaceCallType} from "../../../CallHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import exportConversationalHistory from '../../../utils/exportUtils'; import exportConversationalHistory from '../../../utils/exportUtils/exportUtils';
import { exportFormats, exportOptions } from '../../../utils/exportUtils/exportUtils';
@replaceableComponent("views.rooms.RoomHeader") @replaceableComponent("views.rooms.RoomHeader")
@ -120,7 +121,7 @@ export default class RoomHeader extends React.Component {
} }
_exportConvertionalHistory = async () => { _exportConvertionalHistory = async () => {
exportConversationalHistory(this.props.room); exportConversationalHistory(this.props.room, exportFormats.HTML, exportOptions.TIMELINE);
} }
render() { render() {

View file

@ -1,13 +1,13 @@
import { MatrixClientPeg } from "../MatrixClientPeg";
import { arrayFastClone } from "./arrays";
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) => (` import streamSaver from "streamsaver";
import JSZip from "jszip";
import { decryptFile } from "../DecryptFile";
import { mediaFromContent, mediaFromMxc } from "../../customisations/Media";
import { textForEvent } from "../../TextForEvent";
import Room from 'matrix-js-sdk/src/models/room';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
const wrapHTML = (content: string, room: Room) => (`
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -270,59 +270,27 @@ 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, 30);
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 = [ const userColors = [
"#64bf47", "#64bf47",
"#4f9cd9", "#4f9cd9",
"#9884e8", "#9884e8",
"#e671a5",
"#47bcd1",
"#ff8c44",
]; ];
//Get a color associated with string length. 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) => { const getUserColor = (userId: string) => {
return userColors[userId.length % 4]; return userColors[userId.length % 4];
}; };
const getUserPic = async (event) => { const getUserPic = async (event: MatrixEvent) => {
const member = event.sender; const member = event.sender;
if (!member.getMxcAvatarUrl()) { if (!member.getMxcAvatarUrl()) {
return ` return `
<div class="pull_left userpic_wrap"> <div class="pull_left userpic_wrap">
<div class="userpic" style="width: 42px;height: 42px;background-color: ${getUserColor(member.userId)}"> <div class="userpic" style="width: 42px;height: 42px;background-color: ${getUserColor(member.userId)}">
<div class="initials" style="line-height: 42px;" src="users/${member.userId}">${event.sender.name[0]}</div> <div class="initials" style="line-height: 42px;" src="users/${member.userId}">
${event.sender.name[0]}
</div>;
</div> </div>
</div> </div>
`; `;
@ -347,7 +315,7 @@ const getUserPic = async (event) => {
}; };
//Gets the event_id of an event to which an event is replied //Gets the event_id of an event to which an event is replied
const getBaseEventId = (event) => { const getBaseEventId = (event: MatrixEvent) => {
const isEncrypted = event.isEncrypted(); const isEncrypted = event.isEncrypted();
// If encrypted, in_reply_to lies in event.event.content // If encrypted, in_reply_to lies in event.event.content
@ -357,7 +325,7 @@ const getBaseEventId = (event) => {
}; };
const dateSeparator = (event, prevEvent) => { const dateSeparator = (event: MatrixEvent, prevEvent: MatrixEvent) => {
const prevDate = prevEvent ? new Date(prevEvent.getTs()) : null; const prevDate = prevEvent ? new Date(prevEvent.getTs()) : null;
const currDate = new Date(event.getTs()); const currDate = new Date(event.getTs());
if (!prevDate || currDate.setHours(0, 0, 0, 0) !== prevDate.setHours(0, 0, 0, 0)) { if (!prevDate || currDate.setHours(0, 0, 0, 0) !== prevDate.setHours(0, 0, 0, 0)) {
@ -373,8 +341,8 @@ const dateSeparator = (event, prevEvent) => {
return ""; return "";
}; };
const getImageData = async (event) => { const getImageData = async (event: MatrixEvent) => {
let blob; let blob: Blob;
try { try {
const isEncrypted = event.isEncrypted(); const isEncrypted = event.isEncrypted();
const content = event.getContent(); const content = event.getContent();
@ -383,7 +351,7 @@ const getImageData = async (event) => {
} else { } else {
const media = mediaFromContent(event.getContent()); const media = mediaFromContent(event.getContent());
const image = await fetch(media.srcHttp); const image = await fetch(media.srcHttp);
blob = image.blob(); blob = await image.blob();
} }
} catch (err) { } catch (err) {
console.log("Error decrypting image"); console.log("Error decrypting image");
@ -392,7 +360,7 @@ const getImageData = async (event) => {
}; };
const createMessageBody = async (event, joined = false, isReply = false, replyId = null) => { const createMessageBody = async (event: MatrixEvent, joined = false, isReply = false, replyId = null) => {
const userPic = await getUserPic(event); const userPic = await getUserPic(event);
let messageBody = ""; let messageBody = "";
switch (event.getContent().msgtype) { switch (event.getContent().msgtype) {
@ -400,9 +368,10 @@ const createMessageBody = async (event, joined = false, isReply = false, replyId
messageBody = `<div class="text"> ${event.getContent().body} </div>`; messageBody = `<div class="text"> ${event.getContent().body} </div>`;
break; break;
case "m.image": { case "m.image": {
messageBody = `<a class="photo_wrap clearfix pull_left" href="images/${event.getId()}.png"> messageBody = `
<img class="photo" src="images/${event.getId()}.png" style="max-width: 600px; max-height: 500px;"> <a class="photo_wrap clearfix pull_left" href="images/${event.getId()}.png">
</a>`; <img class="photo" src="images/${event.getId()}.png" style="max-width: 600px; max-height: 500px;">
</a>`;
const blob = await getImageData(event); const blob = await getImageData(event);
zip.file(`images/${event.getId()}.png`, blob); zip.file(`images/${event.getId()}.png`, blob);
} }
@ -412,10 +381,12 @@ const createMessageBody = async (event, joined = false, isReply = false, replyId
} }
return ` return `
<div class="message default clearfix ${joined ? `joined` : ``}" id="message2680"> <div class="message default clearfix ${joined ? `joined` : ``}" id="${event.getId()}">
${!joined ? userPic : ``} ${!joined ? userPic : ``}
<div class="body"> <div class="body">
<div class="pull_right date details" title="${new Date(event.getTs())}">${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}</div> <div class="pull_right date details" title="${new Date(event.getTs())}">
${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}
</div>;
${!joined ? `<div class="from_name" style="color:${getUserColor(event.sender.name)}"> ${!joined ? `<div class="from_name" style="color:${getUserColor(event.sender.name)}">
${event.sender.name} ${event.sender.name}
</div>`: ``} </div>`: ``}
@ -430,7 +401,7 @@ const createMessageBody = async (event, joined = false, isReply = false, replyId
}; };
const createHTML = async (events, room) => { const createHTML = async (events: MatrixEvent[], room: Room) => {
let content = ""; let content = "";
let prevEvent = null; let prevEvent = null;
for (const event of events) { for (const event of events) {
@ -458,19 +429,19 @@ const createHTML = async (events, room) => {
return wrapHTML(content, room); return wrapHTML(content, room);
}; };
const avatars = new Map(); const avatars = new Map();
let zip; let zip;
const exportConversationalHistory = async (room) => {
const res = getTimelineConversation(room); const exportAsHTML = async (res: MatrixEvent[], room: Room) => {
zip = new JSZip(); zip = new JSZip();
const html = await createHTML(res, room, avatars); const html = await createHTML(res, room);
zip.file("index.html", html); zip.file("index.html", html);
zip.file("css/style.css", css); zip.file("css/style.css", css);
avatars.clear(); avatars.clear();
const filename = `matrix-export-${new Date().toISOString()}.zip`; const filename = `matrix-export-${new Date().toISOString()}.zip`;
//Generate the zip file asynchronously //Generate the zip file asynchronously
@ -480,16 +451,17 @@ const exportConversationalHistory = async (room) => {
const fileStream = streamSaver.createWriteStream(filename, blob.size); const fileStream = streamSaver.createWriteStream(filename, blob.size);
const writer = fileStream.getWriter(); const writer = fileStream.getWriter();
// Here we chunk the blob into pieces of 10 MiB // Here we chunk the blob into pieces of 10 MB, the size might be dynamically generated.
// This can be used to keep track of the progress
const sliceSize = 10 * 1e6; const sliceSize = 10 * 1e6;
for (let fPointer = 0; fPointer < blob.size; fPointer += sliceSize) { for (let fPointer = 0; fPointer < blob.size; fPointer += sliceSize) {
// console.log(fPointer); // console.log(fPointer);
const blobPiece = blob.slice(fPointer, fPointer + sliceSize); const blobPiece = blob.slice(fPointer, fPointer + sliceSize);
const reader = new FileReader(); const reader = new FileReader();
const waiter = new Promise((resolve, reject) => { const waiter = new Promise<void>((resolve, reject) => {
reader.onloadend = evt => { reader.onloadend = evt => {
const arrayBufferNew = evt.target.result; const arrayBufferNew: any = evt.target.result;
const uint8ArrayNew = new Uint8Array(arrayBufferNew); const uint8ArrayNew = new Uint8Array(arrayBufferNew);
// Buffer.from(reader.result) // Buffer.from(reader.result)
writer.write(uint8ArrayNew); writer.write(uint8ArrayNew);
@ -500,6 +472,6 @@ const exportConversationalHistory = async (room) => {
await waiter; await waiter;
} }
writer.close(); writer.close();
}; }
export default exportConversationalHistory; export default exportAsHTML;

View file

@ -0,0 +1,56 @@
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { arrayFastClone } from "../arrays";
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
import exportAsHTML from "./HtmlExport";
export const exportFormats = Object.freeze({
"HTML": "HTML",
"JSON": "JSON",
"LOGS": "LOGS",
});
export const exportOptions = Object.freeze({
"TIMELINE": "TIMELINE",
});
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);
});
console.log(events);
return events;
};
const exportConversationalHistory = async (room, format, options) => {
const res = getTimelineConversation(room);
switch (format) {
case exportFormats.HTML:
await exportAsHTML(res, room);
break;
case exportFormats.JSON:
break;
case exportFormats.LOGS:
break;
}
};
export default exportConversationalHistory;