diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index d6b4822cc2..ed2ca07ff5 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -32,7 +32,8 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import {PlaceCallType} from "../../../CallHandler"; 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") @@ -120,7 +121,7 @@ export default class RoomHeader extends React.Component { } _exportConvertionalHistory = async () => { - exportConversationalHistory(this.props.room); + exportConversationalHistory(this.props.room, exportFormats.HTML, exportOptions.TIMELINE); } render() { diff --git a/src/utils/exportUtils.js b/src/utils/exportUtils/HtmlExport.ts similarity index 80% rename from src/utils/exportUtils.js rename to src/utils/exportUtils/HtmlExport.ts index 0ab65df911..1b0d59ab6b 100644 --- a/src/utils/exportUtils.js +++ b/src/utils/exportUtils/HtmlExport.ts @@ -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) => (` @@ -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 = [ "#64bf47", "#4f9cd9", "#9884e8", - "#e671a5", - "#47bcd1", - "#ff8c44", ]; - //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]; }; -const getUserPic = async (event) => { +const getUserPic = async (event: MatrixEvent) => { const member = event.sender; if (!member.getMxcAvatarUrl()) { return `
-
${event.sender.name[0]}
+
+ ${event.sender.name[0]} +
;
`; @@ -347,7 +315,7 @@ const getUserPic = async (event) => { }; //Gets the event_id of an event to which an event is replied -const getBaseEventId = (event) => { +const getBaseEventId = (event: MatrixEvent) => { const isEncrypted = event.isEncrypted(); // 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 currDate = new Date(event.getTs()); if (!prevDate || currDate.setHours(0, 0, 0, 0) !== prevDate.setHours(0, 0, 0, 0)) { @@ -373,8 +341,8 @@ const dateSeparator = (event, prevEvent) => { return ""; }; -const getImageData = async (event) => { - let blob; +const getImageData = async (event: MatrixEvent) => { + let blob: Blob; try { const isEncrypted = event.isEncrypted(); const content = event.getContent(); @@ -383,7 +351,7 @@ const getImageData = async (event) => { } else { const media = mediaFromContent(event.getContent()); const image = await fetch(media.srcHttp); - blob = image.blob(); + blob = await image.blob(); } } catch (err) { 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); let messageBody = ""; switch (event.getContent().msgtype) { @@ -400,9 +368,10 @@ const createMessageBody = async (event, joined = false, isReply = false, replyId messageBody = `
${event.getContent().body}
`; break; case "m.image": { - messageBody = ` - - `; + messageBody = ` + + + `; const blob = await getImageData(event); zip.file(`images/${event.getId()}.png`, blob); } @@ -412,10 +381,12 @@ const createMessageBody = async (event, joined = false, isReply = false, replyId } return ` -
+
${!joined ? userPic : ``}
-
${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}
+
+ ${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)} +
; ${!joined ? `
${event.sender.name}
`: ``} @@ -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 prevEvent = null; for (const event of events) { @@ -458,19 +429,19 @@ const createHTML = async (events, room) => { return wrapHTML(content, room); }; - const avatars = new Map(); let zip; -const exportConversationalHistory = async (room) => { - const res = getTimelineConversation(room); + +const exportAsHTML = async (res: MatrixEvent[], room: Room) => { zip = new JSZip(); - const html = await createHTML(res, room, avatars); + const html = await createHTML(res, room); 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 @@ -480,16 +451,17 @@ const exportConversationalHistory = async (room) => { const fileStream = streamSaver.createWriteStream(filename, blob.size); 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; for (let fPointer = 0; fPointer < blob.size; fPointer += sliceSize) { // console.log(fPointer); const blobPiece = blob.slice(fPointer, fPointer + sliceSize); const reader = new FileReader(); - const waiter = new Promise((resolve, reject) => { + const waiter = new Promise((resolve, reject) => { reader.onloadend = evt => { - const arrayBufferNew = evt.target.result; + const arrayBufferNew: any = evt.target.result; const uint8ArrayNew = new Uint8Array(arrayBufferNew); // Buffer.from(reader.result) writer.write(uint8ArrayNew); @@ -500,6 +472,6 @@ const exportConversationalHistory = async (room) => { await waiter; } writer.close(); -}; +} -export default exportConversationalHistory; +export default exportAsHTML; diff --git a/src/utils/exportUtils/exportUtils.js b/src/utils/exportUtils/exportUtils.js new file mode 100644 index 0000000000..38abe62d74 --- /dev/null +++ b/src/utils/exportUtils/exportUtils.js @@ -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;