Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18969
Conflicts: src/components/views/right_panel/UserInfo.tsx
This commit is contained in:
commit
111ae75874
133 changed files with 5419 additions and 1655 deletions
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
@ -73,9 +72,15 @@ export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
|
|||
}
|
||||
|
||||
const MAX_JUMP_DISTANCE = 100;
|
||||
export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
|
||||
const liveTimeline = room.getLiveTimeline();
|
||||
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
|
||||
export function findEditableEvent({
|
||||
events,
|
||||
isForward,
|
||||
fromEventId,
|
||||
}: {
|
||||
events: MatrixEvent[];
|
||||
isForward: boolean;
|
||||
fromEventId?: string;
|
||||
}): MatrixEvent {
|
||||
const maxIdx = events.length - 1;
|
||||
const inc = isForward ? 1 : -1;
|
||||
const beginIdx = isForward ? 0 : maxIdx;
|
||||
|
|
|
@ -62,8 +62,9 @@ export default class MultiInviter {
|
|||
|
||||
/**
|
||||
* @param {string} targetId The ID of the room or group to invite to
|
||||
* @param {function} progressCallback optional callback, fired after each invite.
|
||||
*/
|
||||
constructor(targetId: string) {
|
||||
constructor(targetId: string, private readonly progressCallback?: () => void) {
|
||||
if (targetId[0] === '+') {
|
||||
this.roomId = null;
|
||||
this.groupId = targetId;
|
||||
|
@ -181,6 +182,7 @@ export default class MultiInviter {
|
|||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
this.progressCallback?.();
|
||||
}).catch((err) => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
|
|
267
src/utils/exportUtils/Exporter.ts
Normal file
267
src/utils/exportUtils/Exporter.ts
Normal file
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { IExportOptions, ExportType } from "./exportUtils";
|
||||
import { decryptFile } from "../DecryptFile";
|
||||
import { mediaFromContent } from "../../customisations/Media";
|
||||
import { formatFullDateNoDay } from "../../DateUtils";
|
||||
import { isVoiceMessage } from "../EventUtils";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
||||
import { IMediaEventContent } from "../../customisations/models/IMediaEventContent";
|
||||
import { saveAs } from "file-saver";
|
||||
import { _t } from "../../languageHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
type BlobFile = {
|
||||
name: string;
|
||||
blob: Blob;
|
||||
};
|
||||
|
||||
export default abstract class Exporter {
|
||||
protected files: BlobFile[] = [];
|
||||
protected client: MatrixClient;
|
||||
protected cancelled = false;
|
||||
|
||||
protected constructor(
|
||||
protected room: Room,
|
||||
protected exportType: ExportType,
|
||||
protected exportOptions: IExportOptions,
|
||||
protected setProgressText: React.Dispatch<React.SetStateAction<string>>,
|
||||
) {
|
||||
if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB
|
||||
exportOptions.maxSize > 2000 * 1024 * 1024|| // More than ~ 2 GB
|
||||
exportOptions.numberOfMessages > 10**8
|
||||
) {
|
||||
throw new Error("Invalid export options");
|
||||
}
|
||||
this.client = MatrixClientPeg.get();
|
||||
window.addEventListener("beforeunload", this.onBeforeUnload);
|
||||
}
|
||||
|
||||
protected onBeforeUnload(e: BeforeUnloadEvent): string {
|
||||
e.preventDefault();
|
||||
return e.returnValue = _t("Are you sure you want to exit during this export?");
|
||||
}
|
||||
|
||||
protected updateProgress(progress: string, log = true, show = true): void {
|
||||
if (log) console.log(progress);
|
||||
if (show) this.setProgressText(progress);
|
||||
}
|
||||
|
||||
protected addFile(filePath: string, blob: Blob): void {
|
||||
const file = {
|
||||
name: filePath,
|
||||
blob,
|
||||
};
|
||||
this.files.push(file);
|
||||
}
|
||||
|
||||
protected async downloadZIP(): Promise<string | void> {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const filename = `${brand} - Chat Export - ${formatFullDateNoDay(new Date())}.zip`;
|
||||
const { default: JSZip } = await import('jszip');
|
||||
|
||||
const zip = new JSZip();
|
||||
// Create a writable stream to the directory
|
||||
if (!this.cancelled) this.updateProgress("Generating a ZIP");
|
||||
else return this.cleanUp();
|
||||
|
||||
for (const file of this.files) zip.file(file.name, file.blob);
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
saveAs(content, filename);
|
||||
}
|
||||
|
||||
protected cleanUp(): string {
|
||||
console.log("Cleaning up...");
|
||||
window.removeEventListener("beforeunload", this.onBeforeUnload);
|
||||
return "";
|
||||
}
|
||||
|
||||
public async cancelExport(): Promise<void> {
|
||||
console.log("Cancelling export...");
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
protected downloadPlainText(fileName: string, text: string) {
|
||||
const content = new Blob([text], { type: "text" });
|
||||
saveAs(content, fileName);
|
||||
}
|
||||
|
||||
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
||||
const roomState = this.client.getRoom(this.room.roomId).currentState;
|
||||
event.sender = roomState.getSentinelMember(
|
||||
event.getSender(),
|
||||
);
|
||||
if (event.getType() === "m.room.member") {
|
||||
event.target = roomState.getSentinelMember(
|
||||
event.getStateKey(),
|
||||
);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
public getLimit(): number {
|
||||
let limit: number;
|
||||
switch (this.exportType) {
|
||||
case ExportType.LastNMessages:
|
||||
limit = this.exportOptions.numberOfMessages;
|
||||
break;
|
||||
case ExportType.Timeline:
|
||||
limit = 40;
|
||||
break;
|
||||
default:
|
||||
limit = 10**8;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
protected async getRequiredEvents(): Promise<MatrixEvent[]> {
|
||||
const eventMapper = this.client.getEventMapper();
|
||||
|
||||
let prevToken: string|null = null;
|
||||
let limit = this.getLimit();
|
||||
const events: MatrixEvent[] = [];
|
||||
|
||||
while (limit) {
|
||||
const eventsPerCrawl = Math.min(limit, 1000);
|
||||
const res = await this.client.createMessagesRequest(
|
||||
this.room.roomId,
|
||||
prevToken,
|
||||
eventsPerCrawl,
|
||||
Direction.Backward,
|
||||
);
|
||||
|
||||
if (this.cancelled) {
|
||||
this.cleanUp();
|
||||
return [];
|
||||
}
|
||||
|
||||
if (res.chunk.length === 0) break;
|
||||
|
||||
limit -= res.chunk.length;
|
||||
|
||||
const matrixEvents: MatrixEvent[] = res.chunk.map(eventMapper);
|
||||
|
||||
for (const mxEv of matrixEvents) {
|
||||
// if (this.exportOptions.startDate && mxEv.getTs() < this.exportOptions.startDate) {
|
||||
// // Once the last message received is older than the start date, we break out of both the loops
|
||||
// limit = 0;
|
||||
// break;
|
||||
// }
|
||||
events.push(mxEv);
|
||||
}
|
||||
this.updateProgress(
|
||||
("Fetched " + events.length + " events ") + (this.exportType === ExportType.LastNMessages
|
||||
? `out of ${this.exportOptions.numberOfMessages}`
|
||||
: "so far"),
|
||||
);
|
||||
prevToken = res.end;
|
||||
}
|
||||
// Reverse the events so that we preserve the order
|
||||
for (let i = 0; i < Math.floor(events.length/2); i++) {
|
||||
[events[i], events[events.length - i - 1]] = [events[events.length - i - 1], events[i]];
|
||||
}
|
||||
|
||||
const decryptionPromises = events
|
||||
.filter(event => event.isEncrypted())
|
||||
.map(event => {
|
||||
return this.client.decryptEventIfNeeded(event, {
|
||||
isRetry: true,
|
||||
emit: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all the events to get decrypted.
|
||||
await Promise.all(decryptionPromises);
|
||||
|
||||
for (let i = 0; i < events.length; i++) this.setEventMetadata(events[i]);
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
protected async getMediaBlob(event: MatrixEvent): Promise<Blob> {
|
||||
let blob: Blob;
|
||||
try {
|
||||
const isEncrypted = event.isEncrypted();
|
||||
const content: IMediaEventContent = event.getContent();
|
||||
const shouldDecrypt = isEncrypted && content.hasOwnProperty("file") && event.getType() !== "m.sticker";
|
||||
if (shouldDecrypt) {
|
||||
blob = await decryptFile(content.file);
|
||||
} else {
|
||||
const media = mediaFromContent(content);
|
||||
const image = await fetch(media.srcHttp);
|
||||
blob = await image.blob();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error decrypting media");
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
public splitFileName(file: string): string[] {
|
||||
const lastDot = file.lastIndexOf('.');
|
||||
if (lastDot === -1) return [file, ""];
|
||||
const fileName = file.slice(0, lastDot);
|
||||
const ext = file.slice(lastDot + 1);
|
||||
return [fileName, '.' + ext];
|
||||
}
|
||||
|
||||
public getFilePath(event: MatrixEvent): string {
|
||||
const mediaType = event.getContent().msgtype;
|
||||
let fileDirectory: string;
|
||||
switch (mediaType) {
|
||||
case "m.image":
|
||||
fileDirectory = "images";
|
||||
break;
|
||||
case "m.video":
|
||||
fileDirectory = "videos";
|
||||
break;
|
||||
case "m.audio":
|
||||
fileDirectory = "audio";
|
||||
break;
|
||||
default:
|
||||
fileDirectory = event.getType() === "m.sticker" ? "stickers" : "files";
|
||||
}
|
||||
const fileDate = formatFullDateNoDay(new Date(event.getTs()));
|
||||
let [fileName, fileExt] = this.splitFileName(event.getContent().body);
|
||||
|
||||
if (event.getType() === "m.sticker") fileExt = ".png";
|
||||
if (isVoiceMessage(event)) fileExt = ".ogg";
|
||||
|
||||
return fileDirectory + "/" + fileName + '-' + fileDate + fileExt;
|
||||
}
|
||||
|
||||
protected isReply(event: MatrixEvent): boolean {
|
||||
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"]);
|
||||
}
|
||||
|
||||
protected isAttachment(mxEv: MatrixEvent): boolean {
|
||||
const attachmentTypes = ["m.sticker", "m.image", "m.file", "m.video", "m.audio"];
|
||||
return mxEv.getType() === attachmentTypes[0] || attachmentTypes.includes(mxEv.getContent().msgtype);
|
||||
}
|
||||
|
||||
abstract export(): Promise<void>;
|
||||
}
|
442
src/utils/exportUtils/HtmlExport.tsx
Normal file
442
src/utils/exportUtils/HtmlExport.tsx
Normal file
|
@ -0,0 +1,442 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Exporter from "./Exporter";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { shouldFormContinuation } from "../../components/structures/MessagePanel";
|
||||
import { formatFullDateNoDayNoTime, wantsDateSeparator } from "../../DateUtils";
|
||||
import { RoomPermalinkCreator } from "../permalinks/Permalinks";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import * as Avatar from "../../Avatar";
|
||||
import EventTile, { haveTileForEvent } from "../../components/views/rooms/EventTile";
|
||||
import DateSeparator from "../../components/views/messages/DateSeparator";
|
||||
import BaseAvatar from "../../components/views/avatars/BaseAvatar";
|
||||
import exportJS from "!!raw-loader!./exportJS";
|
||||
import { ExportType } from "./exportUtils";
|
||||
import { IExportOptions } from "./exportUtils";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import getExportCSS from "./exportCSS";
|
||||
import { textForEvent } from "../../TextForEvent";
|
||||
|
||||
export default class HTMLExporter extends Exporter {
|
||||
protected avatars: Map<string, boolean>;
|
||||
protected permalinkCreator: RoomPermalinkCreator;
|
||||
protected totalSize: number;
|
||||
protected mediaOmitText: string;
|
||||
|
||||
constructor(
|
||||
room: Room,
|
||||
exportType: ExportType,
|
||||
exportOptions: IExportOptions,
|
||||
setProgressText: React.Dispatch<React.SetStateAction<string>>,
|
||||
) {
|
||||
super(room, exportType, exportOptions, setProgressText);
|
||||
this.avatars = new Map<string, boolean>();
|
||||
this.permalinkCreator = new RoomPermalinkCreator(this.room);
|
||||
this.totalSize = 0;
|
||||
this.mediaOmitText = !this.exportOptions.attachmentsIncluded
|
||||
? _t("Media omitted")
|
||||
: _t("Media omitted - file size limit exceeded");
|
||||
}
|
||||
|
||||
protected async getRoomAvatar() {
|
||||
let blob: Blob;
|
||||
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
|
||||
const avatarPath = "room.png";
|
||||
if (avatarUrl) {
|
||||
try {
|
||||
const image = await fetch(avatarUrl);
|
||||
blob = await image.blob();
|
||||
this.totalSize += blob.size;
|
||||
this.addFile(avatarPath, blob);
|
||||
} catch (err) {
|
||||
console.log("Failed to fetch room's avatar" + err);
|
||||
}
|
||||
}
|
||||
const avatar = (
|
||||
<BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
name={this.room.name}
|
||||
title={this.room.name}
|
||||
url={blob ? avatarPath : null}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
);
|
||||
return renderToStaticMarkup(avatar);
|
||||
}
|
||||
|
||||
protected async wrapHTML(content: string) {
|
||||
const roomAvatar = await this.getRoomAvatar();
|
||||
const exportDate = formatFullDateNoDayNoTime(new Date());
|
||||
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator;
|
||||
const exporter = this.client.getUserId();
|
||||
const exporterName = this.room?.getMember(exporter)?.rawDisplayName;
|
||||
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
|
||||
const createdText = _t("%(creatorName)s created this room.", {
|
||||
creatorName,
|
||||
});
|
||||
|
||||
const exportedText = renderToStaticMarkup(
|
||||
<p>
|
||||
{ _t(
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.",
|
||||
{
|
||||
exportDate,
|
||||
},
|
||||
{
|
||||
roomName: () => <b>{ this.room.name }</b>,
|
||||
exporterDetails: () => (
|
||||
<a
|
||||
href={`https://matrix.to/#/${exporter}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ exporterName ? (
|
||||
<>
|
||||
<b>{ exporterName }</b>
|
||||
{ " (" + exporter + ")" }
|
||||
</>
|
||||
) : (
|
||||
<b>{ exporter }</b>
|
||||
) }
|
||||
</a>
|
||||
),
|
||||
},
|
||||
) }
|
||||
</p>,
|
||||
);
|
||||
|
||||
const topicText = topic ? _t("Topic: %(topic)s", { topic }) : "";
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
<script src="js/script.js"></script>
|
||||
<title>Exported Data</title>
|
||||
</head>
|
||||
<body style="height: 100vh;">
|
||||
<section
|
||||
id="matrixchat"
|
||||
style="height: 100%; overflow: auto"
|
||||
class="notranslate"
|
||||
>
|
||||
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
||||
<div class="mx_MatrixChat">
|
||||
<main class="mx_RoomView">
|
||||
<div class="mx_RoomHeader light-panel">
|
||||
<div class="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||
<div class="mx_RoomHeader_avatar">
|
||||
<div class="mx_DecoratedRoomAvatar">
|
||||
${roomAvatar}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_RoomHeader_name">
|
||||
<div
|
||||
dir="auto"
|
||||
class="mx_RoomHeader_nametext"
|
||||
title="${this.room.name}"
|
||||
>
|
||||
${this.room.name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_RoomHeader_topic" dir="auto"> ${topic} </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_MainSplit">
|
||||
<div class="mx_RoomView_body">
|
||||
<div
|
||||
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
mx_AutoHideScrollbar
|
||||
mx_ScrollPanel
|
||||
mx_RoomView_messagePanel
|
||||
mx_GroupLayout
|
||||
"
|
||||
>
|
||||
<div class="mx_RoomView_messageListWrapper">
|
||||
<ol
|
||||
class="mx_RoomView_MessageList"
|
||||
aria-live="polite"
|
||||
role="list"
|
||||
>
|
||||
<div class="mx_NewRoomIntro">
|
||||
${roomAvatar}
|
||||
<h2> ${this.room.name} </h2>
|
||||
<p> ${createdText} <br/><br/> ${exportedText} </p>
|
||||
<br/>
|
||||
<p> ${topicText} </p>
|
||||
</div>
|
||||
${content}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_RoomView_statusArea">
|
||||
<div class="mx_RoomView_statusAreaBox">
|
||||
<div class="mx_RoomView_statusAreaBox_line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="snackbar"/>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
protected getAvatarURL(event: MatrixEvent): string {
|
||||
const member = event.sender;
|
||||
return (
|
||||
member.getMxcAvatarUrl() &&
|
||||
mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||
30,
|
||||
30,
|
||||
"crop",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected async saveAvatarIfNeeded(event: MatrixEvent) {
|
||||
const member = event.sender;
|
||||
if (!this.avatars.has(member.userId)) {
|
||||
try {
|
||||
const avatarUrl = this.getAvatarURL(event);
|
||||
this.avatars.set(member.userId, true);
|
||||
const image = await fetch(avatarUrl);
|
||||
const blob = await image.blob();
|
||||
this.addFile(`users/${member.userId.replace(/:/g, '-')}.png`, blob);
|
||||
} catch (err) {
|
||||
console.log("Failed to fetch user's avatar" + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getDateSeparator(event: MatrixEvent) {
|
||||
const ts = event.getTs();
|
||||
const dateSeparator = <li key={ts}><DateSeparator forExport={true} key={ts} ts={ts} /></li>;
|
||||
return renderToStaticMarkup(dateSeparator);
|
||||
}
|
||||
|
||||
protected needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent) {
|
||||
if (prevEvent == null) return true;
|
||||
return wantsDateSeparator(prevEvent.getDate(), event.getDate());
|
||||
}
|
||||
|
||||
public getEventTile(mxEv: MatrixEvent, continuation: boolean) {
|
||||
return <div className="mx_Export_EventWrapper" id={mxEv.getId()}>
|
||||
<MatrixClientContext.Provider value={this.client}>
|
||||
<EventTile
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
forExport={true}
|
||||
readReceipts={null}
|
||||
alwaysShowTimestamps={true}
|
||||
readReceiptMap={null}
|
||||
showUrlPreview={false}
|
||||
checkUnmounting={() => false}
|
||||
isTwelveHour={false}
|
||||
last={false}
|
||||
lastInSection={false}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
lastSuccessful={false}
|
||||
isSelectedEvent={false}
|
||||
getRelationsForEvent={null}
|
||||
showReactions={false}
|
||||
layout={Layout.Group}
|
||||
enableFlair={false}
|
||||
showReadReceipts={false}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string) {
|
||||
const hasAvatar = !!this.getAvatarURL(mxEv);
|
||||
if (hasAvatar) await this.saveAvatarIfNeeded(mxEv);
|
||||
const EventTile = this.getEventTile(mxEv, continuation);
|
||||
let eventTileMarkup: string;
|
||||
|
||||
if (
|
||||
mxEv.getContent().msgtype == MsgType.Emote ||
|
||||
mxEv.getContent().msgtype == MsgType.Notice ||
|
||||
mxEv.getContent().msgtype === MsgType.Text
|
||||
) {
|
||||
// to linkify textual events, we'll need lifecycle methods which won't be invoked in renderToString
|
||||
// So, we'll have to render the component into a temporary root element
|
||||
const tempRoot = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
EventTile,
|
||||
tempRoot,
|
||||
);
|
||||
eventTileMarkup = tempRoot.innerHTML;
|
||||
} else {
|
||||
eventTileMarkup = renderToStaticMarkup(EventTile);
|
||||
}
|
||||
|
||||
if (filePath) {
|
||||
const mxc = mxEv.getContent().url || mxEv.getContent().file?.url;
|
||||
eventTileMarkup = eventTileMarkup.split(mxc).join(filePath);
|
||||
}
|
||||
eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, '');
|
||||
if (hasAvatar) {
|
||||
eventTileMarkup = eventTileMarkup.replace(
|
||||
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, '&'),
|
||||
`users/${mxEv.sender.userId.replace(/:/g, "-")}.png`,
|
||||
);
|
||||
}
|
||||
return eventTileMarkup;
|
||||
}
|
||||
|
||||
protected createModifiedEvent(text: string, mxEv: MatrixEvent, italic=true) {
|
||||
const modifiedContent = {
|
||||
msgtype: "m.text",
|
||||
body: `${text}`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `${text}`,
|
||||
};
|
||||
if (italic) {
|
||||
modifiedContent.formatted_body = '<em>' + modifiedContent.formatted_body + '</em>';
|
||||
modifiedContent.body = '*' + modifiedContent.body + '*';
|
||||
}
|
||||
const modifiedEvent = new MatrixEvent();
|
||||
modifiedEvent.event = mxEv.event;
|
||||
modifiedEvent.sender = mxEv.sender;
|
||||
modifiedEvent.event.type = "m.room.message";
|
||||
modifiedEvent.event.content = modifiedContent;
|
||||
return modifiedEvent;
|
||||
}
|
||||
|
||||
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
|
||||
let eventTile: string;
|
||||
try {
|
||||
if (this.isAttachment(mxEv)) {
|
||||
if (this.exportOptions.attachmentsIncluded) {
|
||||
try {
|
||||
const blob = await this.getMediaBlob(mxEv);
|
||||
if (this.totalSize + blob.size > this.exportOptions.maxSize) {
|
||||
eventTile = await this.getEventTileMarkup(
|
||||
this.createModifiedEvent(this.mediaOmitText, mxEv),
|
||||
joined,
|
||||
);
|
||||
} else {
|
||||
this.totalSize += blob.size;
|
||||
const filePath = this.getFilePath(mxEv);
|
||||
eventTile = await this.getEventTileMarkup(mxEv, joined, filePath);
|
||||
if (this.totalSize == this.exportOptions.maxSize) {
|
||||
this.exportOptions.attachmentsIncluded = false;
|
||||
}
|
||||
this.addFile(filePath, blob);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error while fetching file" + e);
|
||||
eventTile = await this.getEventTileMarkup(
|
||||
this.createModifiedEvent(_t("Error fetching file"), mxEv),
|
||||
joined,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eventTile = await this.getEventTileMarkup(
|
||||
this.createModifiedEvent(this.mediaOmitText, mxEv),
|
||||
joined,
|
||||
);
|
||||
}
|
||||
} else eventTile = await this.getEventTileMarkup(mxEv, joined);
|
||||
} catch (e) {
|
||||
// TODO: Handle callEvent errors
|
||||
console.error(e);
|
||||
eventTile = await this.getEventTileMarkup(
|
||||
this.createModifiedEvent(textForEvent(mxEv), mxEv, false),
|
||||
joined,
|
||||
);
|
||||
}
|
||||
|
||||
return eventTile;
|
||||
}
|
||||
|
||||
protected async createHTML(events: MatrixEvent[], start: number) {
|
||||
let content = "";
|
||||
let prevEvent = null;
|
||||
for (let i = start; i < Math.min(start + 1000, events.length); i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(`Processing event ${i + 1} out of ${events.length}`, false, true);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveTileForEvent(event)) continue;
|
||||
|
||||
content += this.needsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : "";
|
||||
const shouldBeJoined = !this.needsDateSeparator(event, prevEvent)
|
||||
&& shouldFormContinuation(prevEvent, event, false);
|
||||
const body = await this.createMessageBody(event, shouldBeJoined);
|
||||
this.totalSize += Buffer.byteLength(body);
|
||||
content += body;
|
||||
prevEvent = event;
|
||||
}
|
||||
return await this.wrapHTML(content);
|
||||
}
|
||||
|
||||
public async export() {
|
||||
this.updateProgress("Starting export...");
|
||||
|
||||
const fetchStart = performance.now();
|
||||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
this.updateProgress(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`, true, false);
|
||||
|
||||
this.updateProgress("Creating HTML...");
|
||||
for (let page = 0; page < res.length / 1000; page++) {
|
||||
const html = await this.createHTML(res, page * 1000);
|
||||
this.addFile(`messages${page ? page + 1 : ""}.html`, new Blob([html]));
|
||||
}
|
||||
const exportCSS = await getExportCSS();
|
||||
this.addFile("css/style.css", new Blob([exportCSS]));
|
||||
this.addFile("js/script.js", new Blob([exportJS]));
|
||||
|
||||
await this.downloadZIP();
|
||||
|
||||
const exportEnd = performance.now();
|
||||
|
||||
if (this.cancelled) {
|
||||
console.info("Export cancelled successfully");
|
||||
} else {
|
||||
this.updateProgress("Export successful!");
|
||||
this.updateProgress(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
122
src/utils/exportUtils/JSONExport.ts
Normal file
122
src/utils/exportUtils/JSONExport.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Exporter from "./Exporter";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { formatFullDateNoDay, formatFullDateNoDayNoTime } from "../../DateUtils";
|
||||
import { haveTileForEvent } from "../../components/views/rooms/EventTile";
|
||||
import { ExportType } from "./exportUtils";
|
||||
import { IExportOptions } from "./exportUtils";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
export default class JSONExporter extends Exporter {
|
||||
protected totalSize = 0;
|
||||
protected messages: Record<string, any>[] = [];
|
||||
|
||||
constructor(
|
||||
room: Room,
|
||||
exportType: ExportType,
|
||||
exportOptions: IExportOptions,
|
||||
setProgressText: React.Dispatch<React.SetStateAction<string>>,
|
||||
) {
|
||||
super(room, exportType, exportOptions, setProgressText);
|
||||
}
|
||||
|
||||
protected createJSONString(): string {
|
||||
const exportDate = formatFullDateNoDayNoTime(new Date());
|
||||
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator;
|
||||
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
|
||||
const exporter = this.client.getUserId();
|
||||
const exporterName = this.room?.getMember(exporter)?.rawDisplayName || exporter;
|
||||
const jsonObject = {
|
||||
room_name: this.room.name,
|
||||
room_creator: creatorName,
|
||||
topic,
|
||||
export_date: exportDate,
|
||||
exported_by: exporterName,
|
||||
messages: this.messages,
|
||||
};
|
||||
return JSON.stringify(jsonObject, null, 2);
|
||||
}
|
||||
|
||||
protected async getJSONString(mxEv: MatrixEvent) {
|
||||
if (this.exportOptions.attachmentsIncluded && this.isAttachment(mxEv)) {
|
||||
try {
|
||||
const blob = await this.getMediaBlob(mxEv);
|
||||
if (this.totalSize + blob.size < this.exportOptions.maxSize) {
|
||||
this.totalSize += blob.size;
|
||||
const filePath = this.getFilePath(mxEv);
|
||||
if (this.totalSize == this.exportOptions.maxSize) {
|
||||
this.exportOptions.attachmentsIncluded = false;
|
||||
}
|
||||
this.addFile(filePath, blob);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error fetching file: " + err);
|
||||
}
|
||||
}
|
||||
const jsonEvent: any = mxEv.toJSON();
|
||||
const clearEvent = mxEv.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
||||
return clearEvent;
|
||||
}
|
||||
|
||||
protected async createOutput(events: MatrixEvent[]) {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(`Processing event ${i + 1} out of ${events.length}`, false, true);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveTileForEvent(event)) continue;
|
||||
this.messages.push(await this.getJSONString(event));
|
||||
}
|
||||
return this.createJSONString();
|
||||
}
|
||||
|
||||
public async export() {
|
||||
console.info("Starting export process...");
|
||||
console.info("Fetching events...");
|
||||
|
||||
const fetchStart = performance.now();
|
||||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
console.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`);
|
||||
|
||||
console.info("Creating output...");
|
||||
const text = await this.createOutput(res);
|
||||
|
||||
if (this.files.length) {
|
||||
this.addFile("export.json", new Blob([text]));
|
||||
await this.downloadZIP();
|
||||
} else {
|
||||
const fileName = `matrix-export-${formatFullDateNoDay(new Date())}.json`;
|
||||
this.downloadPlainText(fileName, text);
|
||||
}
|
||||
|
||||
const exportEnd = performance.now();
|
||||
|
||||
if (this.cancelled) {
|
||||
console.info("Export cancelled successfully");
|
||||
} else {
|
||||
console.info("Export successful!");
|
||||
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
151
src/utils/exportUtils/PlainTextExport.ts
Normal file
151
src/utils/exportUtils/PlainTextExport.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Exporter from "./Exporter";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { formatFullDateNoDay } from "../../DateUtils";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { haveTileForEvent } from "../../components/views/rooms/EventTile";
|
||||
import { ExportType } from "./exportUtils";
|
||||
import { IExportOptions } from "./exportUtils";
|
||||
import { textForEvent } from "../../TextForEvent";
|
||||
|
||||
export default class PlainTextExporter extends Exporter {
|
||||
protected totalSize: number;
|
||||
protected mediaOmitText: string;
|
||||
|
||||
constructor(
|
||||
room: Room,
|
||||
exportType: ExportType,
|
||||
exportOptions: IExportOptions,
|
||||
setProgressText: React.Dispatch<React.SetStateAction<string>>,
|
||||
) {
|
||||
super(room, exportType, exportOptions, setProgressText);
|
||||
this.totalSize = 0;
|
||||
this.mediaOmitText = !this.exportOptions.attachmentsIncluded
|
||||
? _t("Media omitted")
|
||||
: _t("Media omitted - file size limit exceeded");
|
||||
}
|
||||
|
||||
public textForReplyEvent = (content: IContent) => {
|
||||
const REPLY_REGEX = /> <(.*?)>(.*?)\n\n(.*)/s;
|
||||
const REPLY_SOURCE_MAX_LENGTH = 32;
|
||||
|
||||
const match = REPLY_REGEX.exec(content.body);
|
||||
|
||||
// if the reply format is invalid, then return the body
|
||||
if (!match) return content.body;
|
||||
|
||||
let rplSource: string;
|
||||
const rplName = match[1];
|
||||
const rplText = match[3];
|
||||
|
||||
rplSource = match[2].substring(1);
|
||||
// Get the first non-blank line from the source.
|
||||
const lines = rplSource.split('\n').filter((line) => !/^\s*$/.test(line));
|
||||
if (lines.length > 0) {
|
||||
// Cut to a maximum length.
|
||||
rplSource = lines[0].substring(0, REPLY_SOURCE_MAX_LENGTH);
|
||||
// Ellipsis if needed.
|
||||
if (lines[0].length > REPLY_SOURCE_MAX_LENGTH) {
|
||||
rplSource = rplSource + "...";
|
||||
}
|
||||
// Wrap in formatting
|
||||
rplSource = ` "${rplSource}"`;
|
||||
} else {
|
||||
// Don't show a source because we couldn't format one.
|
||||
rplSource = "";
|
||||
}
|
||||
|
||||
return `<${rplName}${rplSource}> ${rplText}`;
|
||||
};
|
||||
|
||||
protected plainTextForEvent = async (mxEv: MatrixEvent) => {
|
||||
const senderDisplayName = mxEv.sender && mxEv.sender.name ? mxEv.sender.name : mxEv.getSender();
|
||||
let mediaText = "";
|
||||
if (this.isAttachment(mxEv)) {
|
||||
if (this.exportOptions.attachmentsIncluded) {
|
||||
try {
|
||||
const blob = await this.getMediaBlob(mxEv);
|
||||
if (this.totalSize + blob.size > this.exportOptions.maxSize) {
|
||||
mediaText = ` (${this.mediaOmitText})`;
|
||||
} else {
|
||||
this.totalSize += blob.size;
|
||||
const filePath = this.getFilePath(mxEv);
|
||||
mediaText = " (" + _t("File Attached") + ")";
|
||||
this.addFile(filePath, blob);
|
||||
if (this.totalSize == this.exportOptions.maxSize) {
|
||||
this.exportOptions.attachmentsIncluded = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
mediaText = " (" + _t("Error fetching file") + ")";
|
||||
console.log("Error fetching file " + error);
|
||||
}
|
||||
} else mediaText = ` (${this.mediaOmitText})`;
|
||||
}
|
||||
if (this.isReply(mxEv)) return senderDisplayName + ": " + this.textForReplyEvent(mxEv.getContent()) + mediaText;
|
||||
else return textForEvent(mxEv) + mediaText;
|
||||
};
|
||||
|
||||
protected async createOutput(events: MatrixEvent[]) {
|
||||
let content = "";
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(`Processing event ${i + 1} out of ${events.length}`, false, true);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveTileForEvent(event)) continue;
|
||||
const textForEvent = await this.plainTextForEvent(event);
|
||||
content += textForEvent && `${new Date(event.getTs()).toLocaleString()} - ${textForEvent}\n`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public async export() {
|
||||
this.updateProgress("Starting export process...");
|
||||
this.updateProgress("Fetching events...");
|
||||
|
||||
const fetchStart = performance.now();
|
||||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
console.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`);
|
||||
|
||||
this.updateProgress("Creating output...");
|
||||
const text = await this.createOutput(res);
|
||||
|
||||
if (this.files.length) {
|
||||
this.addFile("export.txt", new Blob([text]));
|
||||
await this.downloadZIP();
|
||||
} else {
|
||||
const fileName = `matrix-export-${formatFullDateNoDay(new Date())}.txt`;
|
||||
this.downloadPlainText(fileName, text);
|
||||
}
|
||||
|
||||
const exportEnd = performance.now();
|
||||
|
||||
if (this.cancelled) {
|
||||
console.info("Export cancelled successfully");
|
||||
} else {
|
||||
console.info("Export successful!");
|
||||
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
50
src/utils/exportUtils/exportCSS.ts
Normal file
50
src/utils/exportUtils/exportCSS.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len, camelcase */
|
||||
|
||||
import customCSS from "!!raw-loader!./exportCustomCSS.css";
|
||||
|
||||
const getExportCSS = async (): Promise<string> => {
|
||||
const stylesheets: string[] = [];
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((e: any) => {
|
||||
if (e.href.endsWith("bundle.css") || e.href.endsWith("theme-light.css")) {
|
||||
stylesheets.push(e.href);
|
||||
}
|
||||
});
|
||||
let CSS = "";
|
||||
for (const stylesheet of stylesheets) {
|
||||
const res = await fetch(stylesheet);
|
||||
const innerText = await res.text();
|
||||
CSS += innerText;
|
||||
}
|
||||
const fontFaceRegex = /@font-face {.*?}/sg;
|
||||
|
||||
CSS = CSS.replace(fontFaceRegex, '');
|
||||
CSS = CSS.replace(
|
||||
/font-family: (Inter|'Inter')/g,
|
||||
`font-family: -apple-system, BlinkMacSystemFont, avenir next,
|
||||
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`,
|
||||
);
|
||||
CSS = CSS.replace(
|
||||
/font-family: Inconsolata/g,
|
||||
"font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
|
||||
);
|
||||
|
||||
return CSS + customCSS;
|
||||
};
|
||||
|
||||
export default getExportCSS;
|
138
src/utils/exportUtils/exportCustomCSS.css
Normal file
138
src/utils/exportUtils/exportCustomCSS.css
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
This file is raw-imported (imported as plain text) for the export bundle, which is the reason for the .css format and the colours being hard-coded hard-coded.
|
||||
*/
|
||||
|
||||
#snackbar {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
margin-left: -125px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
bottom: 30px;
|
||||
font-size: 17px;
|
||||
padding: 6px 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.43;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.01071em;
|
||||
}
|
||||
|
||||
#snackbar.mx_show {
|
||||
visibility: visible;
|
||||
-webkit-animation: mx_snackbar_fadein 0.5s, mx_snackbar_fadeout 0.5s 2.5s;
|
||||
animation: mx_snackbar_fadein 0.5s, mx_snackbar_fadeout 0.5s 2.5s;
|
||||
}
|
||||
|
||||
a.mx_reply_anchor {
|
||||
cursor: pointer;
|
||||
color: #238cf5;
|
||||
}
|
||||
|
||||
a.mx_reply_anchor:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@-webkit-keyframes mx_snackbar_fadein {
|
||||
from {
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
bottom: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_snackbar_fadein {
|
||||
from {
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
bottom: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes mx_snackbar_fadeout {
|
||||
from {
|
||||
bottom: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_snackbar_fadeout {
|
||||
from {
|
||||
bottom: 30px;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scroll-behavior: smooth !important;
|
||||
}
|
||||
|
||||
.mx_Export_EventWrapper:target {
|
||||
background: white;
|
||||
animation: mx_event_highlight_animation 2s linear;
|
||||
}
|
||||
|
||||
@keyframes mx_event_highlight_animation {
|
||||
0%,
|
||||
100% {
|
||||
background: white;
|
||||
}
|
||||
50% {
|
||||
background: #e3e2df;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ReplyThread_Export {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_RedactedBody {
|
||||
padding-left: unset;
|
||||
}
|
||||
|
||||
img {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_MatrixChat {
|
||||
max-width: 100%;
|
||||
}
|
42
src/utils/exportUtils/exportJS.js
Normal file
42
src/utils/exportUtils/exportJS.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file is raw-imported (imported as plain text) for the export bundle, which is why this is in JS
|
||||
function showToastIfNeeded(replyId) {
|
||||
const el = document.getElementById(replyId);
|
||||
if (!el) {
|
||||
showToast("The message you're looking for wasn't exported");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(text) {
|
||||
const el = document.getElementById("snackbar");
|
||||
el.innerHTML = text;
|
||||
el.className = "mx_show";
|
||||
setTimeout(() => {
|
||||
el.className = el.className.replace("mx_show", "");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
document.querySelectorAll('.mx_reply_anchor').forEach(element => {
|
||||
element.addEventListener('click', event => {
|
||||
showToastIfNeeded(event.target.getAttribute("scroll-to"));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
65
src/utils/exportUtils/exportUtils.ts
Normal file
65
src/utils/exportUtils/exportUtils.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
export enum ExportFormat {
|
||||
Html = "Html",
|
||||
PlainText = "PlainText",
|
||||
Json = "Json",
|
||||
}
|
||||
|
||||
export enum ExportType {
|
||||
Timeline = "Timeline",
|
||||
Beginning = "Beginning",
|
||||
LastNMessages = "LastNMessages",
|
||||
// START_DATE = "START_DATE",
|
||||
}
|
||||
|
||||
export const textForFormat = (format: ExportFormat): string => {
|
||||
switch (format) {
|
||||
case ExportFormat.Html:
|
||||
return _t("HTML");
|
||||
case ExportFormat.Json:
|
||||
return _t("JSON");
|
||||
case ExportFormat.PlainText:
|
||||
return _t("Plain Text");
|
||||
default:
|
||||
throw new Error("Unknown format");
|
||||
}
|
||||
};
|
||||
|
||||
export const textForType = (type: ExportType): string => {
|
||||
switch (type) {
|
||||
case ExportType.Beginning:
|
||||
return _t("From the beginning");
|
||||
case ExportType.LastNMessages:
|
||||
return _t("Specify a number of messages");
|
||||
case ExportType.Timeline:
|
||||
return _t("Current Timeline");
|
||||
default:
|
||||
throw new Error("Unknown type: " + type);
|
||||
// case exportTypes.START_DATE:
|
||||
// return _t("From a specific date");
|
||||
}
|
||||
};
|
||||
|
||||
export interface IExportOptions {
|
||||
// startDate?: number;
|
||||
numberOfMessages?: number;
|
||||
attachmentsIncluded: boolean;
|
||||
maxSize: number;
|
||||
}
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
// Returns a promise which resolves when the input promise resolves with its value
|
||||
// or when the timeout of ms is reached with the value of given timeoutValue
|
||||
export async function timeout<T>(promise: Promise<T>, timeoutValue: T, ms: number): Promise<T> {
|
||||
const timeoutPromise = new Promise<T>((resolve) => {
|
||||
export async function timeout<T, Y>(promise: Promise<T>, timeoutValue: Y, ms: number): Promise<T | Y> {
|
||||
const timeoutPromise = new Promise<T | Y>((resolve) => {
|
||||
const timeoutId = setTimeout(resolve, ms, timeoutValue);
|
||||
promise.then(() => {
|
||||
clearTimeout(timeoutId);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue