mirror of https://github.com/vector-im/riot-web
Rewrite export tool to use existing components to render output, use existing source URLs for media
parent
60ef6f9332
commit
573a3ca983
|
@ -41,7 +41,7 @@ const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
function shouldFormContinuation(prevEvent, mxEvent) {
|
export function shouldFormContinuation(prevEvent, mxEvent) {
|
||||||
// sanity check inputs
|
// sanity check inputs
|
||||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||||
// check if within the max continuation period
|
// check if within the max continuation period
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default class ReplyThread extends React.Component {
|
||||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||||
// Specifies which layout to use.
|
// Specifies which layout to use.
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
|
isExporting: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
@ -67,6 +68,9 @@ export default class ReplyThread extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
|
if (this.props.isExporting) return;
|
||||||
|
|
||||||
this.context.on("Event.replaced", this.onEventReplaced);
|
this.context.on("Event.replaced", this.onEventReplaced);
|
||||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||||
|
@ -212,12 +216,13 @@ export default class ReplyThread extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, isExporting) {
|
||||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||||
}
|
}
|
||||||
return <ReplyThread
|
return <ReplyThread
|
||||||
parentEv={parentEv}
|
parentEv={parentEv}
|
||||||
|
isExporting={isExporting}
|
||||||
onHeightChanged={onHeightChanged}
|
onHeightChanged={onHeightChanged}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
permalinkCreator={permalinkCreator}
|
permalinkCreator={permalinkCreator}
|
||||||
|
@ -366,6 +371,11 @@ export default class ReplyThread extends React.Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</blockquote>;
|
</blockquote>;
|
||||||
|
} else if (this.props.isExporting) {
|
||||||
|
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
|
||||||
|
header = <p style={{ marginTop: -5, marginBottom: 5 }}>
|
||||||
|
In reply to <a className="mx_reply_anchor" scroll-to={`${eventId}`}>this message</a>
|
||||||
|
</p>;
|
||||||
} else if (this.state.loading) {
|
} else if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
header = <Spinner w={16} h={16} />;
|
header = <Spinner w={16} h={16} />;
|
||||||
|
|
|
@ -37,10 +37,15 @@ function getdaysArray() {
|
||||||
export default class DateSeparator extends React.Component {
|
export default class DateSeparator extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ts: PropTypes.number.isRequired,
|
ts: PropTypes.number.isRequired,
|
||||||
|
isExporting: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
getLabel() {
|
getLabel() {
|
||||||
const date = new Date(this.props.ts);
|
const date = new Date(this.props.ts);
|
||||||
|
|
||||||
|
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
|
||||||
|
if (this.props.isExporting) return formatFullDateNoTime(date);
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
const days = getdaysArray();
|
const days = getdaysArray();
|
||||||
|
|
|
@ -249,6 +249,8 @@ interface IProps {
|
||||||
// for now.
|
// for now.
|
||||||
tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
|
tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
|
||||||
|
|
||||||
|
isExporting?: boolean;
|
||||||
|
|
||||||
// show twelve hour timestamps
|
// show twelve hour timestamps
|
||||||
isTwelveHour?: boolean;
|
isTwelveHour?: boolean;
|
||||||
|
|
||||||
|
@ -405,6 +407,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
// TODO: [REACT-WARNING] Move into constructor
|
// TODO: [REACT-WARNING] Move into constructor
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
|
// Context isn't propagated through renderToStaticMarkup so we'll have to explicitly set it during export
|
||||||
|
if (this.props.isExporting) this.context = MatrixClientPeg.get();
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent(this.props.mxEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -607,6 +611,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldHighlight() {
|
shouldHighlight() {
|
||||||
|
if (this.props.isExporting) return false;
|
||||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||||
if (!actions || !actions.tweaks) { return false; }
|
if (!actions || !actions.tweaks) { return false; }
|
||||||
|
|
||||||
|
@ -951,7 +956,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||||
const actionBar = !isEditing ? <MessageActionBar
|
const showMessageActionBar = !isEditing && !this.props.isExporting;
|
||||||
|
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
@ -1127,6 +1133,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
this.replyThread,
|
this.replyThread,
|
||||||
this.props.layout,
|
this.props.layout,
|
||||||
|
this.props.isExporting,
|
||||||
);
|
);
|
||||||
|
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
|
@ -1169,11 +1176,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||||
function isMessageEvent(ev) {
|
function isMessageEvent(ev: MatrixEvent): boolean {
|
||||||
return (messageTypes.includes(ev.getType()));
|
return (messageTypes.includes(ev.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function haveTileForEvent(e) {
|
export function haveTileForEvent(e: MatrixEvent): boolean {
|
||||||
// Only messages have a tile (black-rectangle) if redacted
|
// Only messages have a tile (black-rectangle) if redacted
|
||||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||||
|
|
||||||
|
@ -1244,11 +1251,11 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoverStart = () => {
|
onHoverStart = () => {
|
||||||
this.setState({hover: true});
|
this.setState({ hover: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onHoverEnd = () => {
|
onHoverEnd = () => {
|
||||||
this.setState({hover: false});
|
this.setState({ hover: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -1286,11 +1293,11 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoverStart = () => {
|
onHoverStart = () => {
|
||||||
this.setState({hover: true});
|
this.setState({ hover: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onHoverEnd = () => {
|
onHoverEnd = () => {
|
||||||
this.setState({hover: false});
|
this.setState({ hover: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,639 +0,0 @@
|
||||||
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";
|
|
||||||
import { getUserNameColorClass } from "../FormattingUtils";
|
|
||||||
import { Exporter } from "./Exporter";
|
|
||||||
import * as ponyfill from "web-streams-polyfill/ponyfill"
|
|
||||||
import { sanitizeHtmlParams } from "../../HtmlUtils";
|
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
|
|
||||||
const css = `
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font: 12px/18px 'Inter', 'Open Sans',"Lucida Grande","Lucida Sans Unicode",Arial,Helvetica,Verdana,sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_clearfix:after {
|
|
||||||
content: " ";
|
|
||||||
visibility: hidden;
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_pull_left {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_pull_right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_wrap {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_wrap a {
|
|
||||||
color: #238CF5;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_wrap a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_header {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: #ffffff;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid #e3e6e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_header .mx_content {
|
|
||||||
width: 480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_header a.mx_content {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 24px 21px;
|
|
||||||
background-size: 24px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_bold {
|
|
||||||
color: #212121;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_details {
|
|
||||||
color: #70777b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_header .mx_content .mx_text {
|
|
||||||
padding: 24px 24px 22px 24px;
|
|
||||||
font-size: 22px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_header a.mx_content .mx_text {
|
|
||||||
padding: 24px 24px 22px 82px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_page_body {
|
|
||||||
padding-top: 64px;
|
|
||||||
width: 700px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_userpic {
|
|
||||||
display: block;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_userpic .mx_initials {
|
|
||||||
display: block;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mx_block_link {
|
|
||||||
display: block;
|
|
||||||
text-decoration: none !important;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mx_block_link:hover {
|
|
||||||
text-decoration: none !important;
|
|
||||||
background-color: #f5f7f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_history {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_message {
|
|
||||||
margin: 0 -10px;
|
|
||||||
transition: background-color 1.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.mx_selected {
|
|
||||||
background-color: #eeeeee;
|
|
||||||
transition: background-color 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_service {
|
|
||||||
padding: 10px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_service .mx_body {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_message .mx_userpic .mx_initials {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default.mx_joined {
|
|
||||||
margin-top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_from_name {
|
|
||||||
font-weight: 700;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_body {
|
|
||||||
margin-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_text {
|
|
||||||
word-wrap: break-word;
|
|
||||||
line-height: 150%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_reply_to,
|
|
||||||
.mx_default .mx_media_wrap {
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media {
|
|
||||||
margin: 0 -10px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media .mx_fill,
|
|
||||||
.mx_default .mx_media .mx_thumb {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media .mx_fill {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 12px 12px;
|
|
||||||
background-size: 24px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media .mx_title {
|
|
||||||
padding-top: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media .mx_description {
|
|
||||||
color: #000000;
|
|
||||||
padding-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_media .mx_status {
|
|
||||||
padding-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_default .mx_photo {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color1{
|
|
||||||
color: #368bd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color1{
|
|
||||||
background-color: #368bd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color2{
|
|
||||||
color: #ac3ba8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color2{
|
|
||||||
background-color: #ac3ba8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color3{
|
|
||||||
color: #03b381;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color3{
|
|
||||||
background-color: #03b381;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color4{
|
|
||||||
color: #e64f7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color4{
|
|
||||||
background-color: #e64f7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color5{
|
|
||||||
color: #ff812d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color5{
|
|
||||||
background-color: #ff812d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color6{
|
|
||||||
color: #2dc2c5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color6{
|
|
||||||
background-color: #2dc2c5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color7{
|
|
||||||
color: #5c56f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color7{
|
|
||||||
background-color: #5c56f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_from_name.mx_Username_color8{
|
|
||||||
color: #74d12c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_initials_wrap.mx_Username_color8{
|
|
||||||
background-color: #74d12c;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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-size: 0.875rem;
|
|
||||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.43;
|
|
||||||
border-radius: 4px;
|
|
||||||
letter-spacing: 0.01071em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#snackbar.show {
|
|
||||||
visibility: visible;
|
|
||||||
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
|
||||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadein {
|
|
||||||
from {bottom: 0; opacity: 0;}
|
|
||||||
to {bottom: 30px; opacity: 1;}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
from {bottom: 0; opacity: 0;}
|
|
||||||
to {bottom: 30px; opacity: 1;}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadeout {
|
|
||||||
from {bottom: 30px; opacity: 1;}
|
|
||||||
to {bottom: 0; opacity: 0;}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeout {
|
|
||||||
from {bottom: 30px; opacity: 1;}
|
|
||||||
to {bottom: 0; opacity: 0;}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const js = `
|
|
||||||
function scrollToElement(replyId){
|
|
||||||
let el = document.getElementById(replyId);
|
|
||||||
if(!el) {
|
|
||||||
showToast("The message you're looking for wasn't exported");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
el.classList.add("mx_selected");
|
|
||||||
setTimeout(() => {
|
|
||||||
el.classList.remove("mx_selected");
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(text) {
|
|
||||||
let el = document.getElementById("snackbar");
|
|
||||||
el.innerHTML = text;
|
|
||||||
el.className = "show";
|
|
||||||
setTimeout(() => {
|
|
||||||
el.className = el.className.replace("show", "");
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default class HTMLExporter extends Exporter {
|
|
||||||
protected zip: JSZip;
|
|
||||||
protected avatars: Map<string, boolean>;
|
|
||||||
|
|
||||||
constructor(res: MatrixEvent[], room: Room) {
|
|
||||||
super(res, room);
|
|
||||||
this.zip = new JSZip();
|
|
||||||
this.avatars = new Map<string, boolean>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected wrapHTML(content: string, room: Room) {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Exported Data</title>
|
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
||||||
<link href="css/style.css" rel="stylesheet" />
|
|
||||||
<script src="js/script.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="mx_page_wrap">
|
|
||||||
<div class="mx_page_header">
|
|
||||||
<div class="mx_content">
|
|
||||||
<div class="mx_text mx_bold">${room.name}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mx_page_body mx_chat_page">
|
|
||||||
<div class="mx_history">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="snackbar"/>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
protected isEdit(event: MatrixEvent) {
|
|
||||||
if (event.getType() === "m.room.message" && event.getContent().hasOwnProperty("m.new_content")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getUserAvatar(event: MatrixEvent) {
|
|
||||||
const member = event.sender;
|
|
||||||
if (!member.getMxcAvatarUrl()) {
|
|
||||||
return `
|
|
||||||
<div class="mx_pull_left mx_userpic_wrap">
|
|
||||||
<div
|
|
||||||
class="mx_userpic mx_initials_wrap ${getUserNameColorClass(event.getSender())}"
|
|
||||||
style="width: 42px;height: 42px;"
|
|
||||||
>
|
|
||||||
<div class="mx_initials" style="line-height: 42px;" src="users/${member.userId}">
|
|
||||||
${event.sender.name[0]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
const imageUrl = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(42, 42, "crop");
|
|
||||||
|
|
||||||
if (!this.avatars.has(member.userId)) {
|
|
||||||
this.avatars.set(member.userId, true);
|
|
||||||
const image = await fetch(imageUrl);
|
|
||||||
const blob = await image.blob();
|
|
||||||
this.zip.file(`users/${member.userId}`, blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="mx_pull_left mx_userpic_wrap">
|
|
||||||
<div class="mx_userpic" style="width: 42px; height: 42px;">
|
|
||||||
<img
|
|
||||||
class="mx_initials"
|
|
||||||
style="width: 42px;height: 42px;line-height:42px;"
|
|
||||||
src="users/${member.userId}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getMediaBlob(event: MatrixEvent) {
|
|
||||||
let blob: 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 = await image.blob();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Error decrypting media");
|
|
||||||
}
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Gets the event_id of an event to which an event is replied
|
|
||||||
protected getBaseEventId = (event: MatrixEvent) => {
|
|
||||||
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"]) ? relatesTo["m.in_reply_to"]["event_id"] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected 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)) {
|
|
||||||
return `
|
|
||||||
<div class="mx_message mx_service">
|
|
||||||
<div class="mx_body mx_details">
|
|
||||||
${new Date(event.getTs())
|
|
||||||
.toLocaleString("en-us", {year: "numeric", month: "long", day: "numeric" })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async createMessageBody(event: MatrixEvent, joined = false, isReply = false, replyId = null) {
|
|
||||||
const userPic = await this.getUserAvatar(event);
|
|
||||||
let messageBody = "";
|
|
||||||
switch (event.getContent().msgtype) {
|
|
||||||
case "m.text":
|
|
||||||
messageBody = `
|
|
||||||
<div class="mx_text">
|
|
||||||
${sanitizeHtml(event.getContent().body, sanitizeHtmlParams)}
|
|
||||||
</div>`;
|
|
||||||
break;
|
|
||||||
case "m.emote":
|
|
||||||
messageBody = `
|
|
||||||
<div class="mx_text">
|
|
||||||
* ${event.sender ? event.sender.name : event.getSender()}
|
|
||||||
${sanitizeHtml(event.getContent().body, sanitizeHtmlParams)}
|
|
||||||
</div>`;
|
|
||||||
break;
|
|
||||||
case "m.image": {
|
|
||||||
const blob = await this.getMediaBlob(event);
|
|
||||||
const fileName = `${event.getId()}.${blob.type.replace("image/", "")}`;
|
|
||||||
messageBody = `
|
|
||||||
<a class="mx_photo_wrap mx_clearfix mx_pull_left" href="images/${event.getId()}.png">
|
|
||||||
<img
|
|
||||||
class="mx_photo"
|
|
||||||
style="max-width: 600px; max-height: 500px;"
|
|
||||||
src="images/${fileName}"
|
|
||||||
/>
|
|
||||||
</a>`;
|
|
||||||
this.zip.file(`images/${fileName}`, blob);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "m.video": {
|
|
||||||
const blob = await this.getMediaBlob(event);
|
|
||||||
const fileName = `${event.getId()}.${blob.type.replace("video/", "")}`;
|
|
||||||
messageBody = `
|
|
||||||
<div class="mx_media_wrap mx_clearfix">
|
|
||||||
<video
|
|
||||||
class="mx_video_file"
|
|
||||||
src="videos/${fileName}"
|
|
||||||
style="max-height: 400px; max-width: 600px;"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</div>`;
|
|
||||||
this.zip.file(`videos/${fileName}`, blob);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "m.audio": {
|
|
||||||
const blob = await this.getMediaBlob(event);
|
|
||||||
const fileName = `${event.getId()}.${blob.type.replace("audio/", "")}`;
|
|
||||||
messageBody = `
|
|
||||||
<div class="mx_media_wrap mx_clearfix">
|
|
||||||
<audio
|
|
||||||
class="mx_audio_file"
|
|
||||||
src="audio/${fileName}"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</div>`;
|
|
||||||
this.zip.file(`audio/${fileName}`, blob);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReply) {
|
|
||||||
messageBody = event.getContent().formatted_body.replace(/<mx-reply>.*<\/mx-reply>/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="mx_message mx_default mx_clearfix ${joined ? `mx_joined` : ``}" id="${event.getId()}">
|
|
||||||
${!joined ? userPic : ``}
|
|
||||||
<div class="mx_body">
|
|
||||||
<div class="mx_pull_right mx_date mx_details" title="${new Date(event.getTs())}">
|
|
||||||
${new Date(event.getTs()).toLocaleTimeString().slice(0, -3)}
|
|
||||||
</div>
|
|
||||||
${!joined ? `
|
|
||||||
<div class="mx_from_name ${getUserNameColorClass(event.getSender())}">
|
|
||||||
${event.sender.name}
|
|
||||||
</div>`: ``}
|
|
||||||
${isReply ?
|
|
||||||
` <div class="mx_reply_to mx_details">
|
|
||||||
In reply to <a onclick="scrollToElement('${replyId}')">this message</a>
|
|
||||||
</div>`: ``}
|
|
||||||
${messageBody}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async createHTML(events: MatrixEvent[], room: Room) {
|
|
||||||
let content = "";
|
|
||||||
let prevEvent = null;
|
|
||||||
for (const event of events) {
|
|
||||||
// As the getContent of the edited event fetches the latest edit, there is no need to process edit events
|
|
||||||
if (this.isEdit(event)) continue;
|
|
||||||
content += this.dateSeparator(event, prevEvent);
|
|
||||||
|
|
||||||
if (event.getType() === "m.room.message") {
|
|
||||||
const replyTo = this.getBaseEventId(event);
|
|
||||||
const shouldBeJoined = prevEvent && prevEvent.getContent().msgtype === "m.text"
|
|
||||||
&& event.sender.userId === prevEvent.sender.userId && !this.dateSeparator(event, prevEvent) && !replyTo;
|
|
||||||
|
|
||||||
const body = await this.createMessageBody(event, shouldBeJoined, !!replyTo, replyTo);
|
|
||||||
content += body;
|
|
||||||
} else {
|
|
||||||
const eventText = textForEvent(event);
|
|
||||||
content += eventText ? `
|
|
||||||
<div class="mx_message mx_service" id="${event.getId()}">
|
|
||||||
<div class="mx_body mx_details">
|
|
||||||
${textForEvent(event)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : "";
|
|
||||||
}
|
|
||||||
prevEvent = event;
|
|
||||||
}
|
|
||||||
return this.wrapHTML(content, room);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async export() {
|
|
||||||
const html = await this.createHTML(this.res, this.room);
|
|
||||||
|
|
||||||
this.zip.file("index.html", html);
|
|
||||||
this.zip.file("css/style.css", css);
|
|
||||||
this.zip.file("js/script.js", js);
|
|
||||||
const filename = `matrix-export-${new Date().toISOString()}.zip`;
|
|
||||||
|
|
||||||
//Generate the zip file asynchronously
|
|
||||||
const blob = await this.zip.generateAsync({ type: "blob" });
|
|
||||||
|
|
||||||
//Support for firefox browser
|
|
||||||
streamSaver.WritableStream = ponyfill.WritableStream
|
|
||||||
//Create a writable stream to the directory
|
|
||||||
const fileStream = streamSaver.createWriteStream(filename, { size: blob.size });
|
|
||||||
const writer = fileStream.getWriter();
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
const blobPiece = blob.slice(fPointer, fPointer + sliceSize);
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
const waiter = new Promise<void>((resolve, reject) => {
|
|
||||||
reader.onloadend = evt => {
|
|
||||||
const arrayBufferNew: any = evt.target.result;
|
|
||||||
const uint8ArrayNew = new Uint8Array(arrayBufferNew);
|
|
||||||
writer.write(uint8ArrayNew);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(blobPiece);
|
|
||||||
});
|
|
||||||
await waiter;
|
|
||||||
}
|
|
||||||
writer.close();
|
|
||||||
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
import React from "react"
|
||||||
|
import streamSaver from "streamsaver";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { decryptFile } from "../DecryptFile";
|
||||||
|
import { mediaFromContent } from "../../customisations/Media";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Exporter } from "./Exporter";
|
||||||
|
import * as ponyfill from "web-streams-polyfill/ponyfill"
|
||||||
|
import { renderToStaticMarkup } from 'react-dom/server'
|
||||||
|
import { Layout } from "../../settings/Layout";
|
||||||
|
import { shouldFormContinuation } from "../../components/structures/MessagePanel";
|
||||||
|
import { wantsDateSeparator } from "../../DateUtils";
|
||||||
|
import { RoomPermalinkCreator } from "../permalinks/Permalinks";
|
||||||
|
import EventTile, { haveTileForEvent } from "../../components/views/rooms/EventTile";
|
||||||
|
import DateSeparator from "../../components/views/messages/DateSeparator";
|
||||||
|
import exportCSS from "./exportCSS";
|
||||||
|
import exportJS from "./exportJS";
|
||||||
|
|
||||||
|
export default class HTMLExporter extends Exporter {
|
||||||
|
protected zip: JSZip;
|
||||||
|
protected avatars: Map<string, boolean>;
|
||||||
|
|
||||||
|
constructor(res: MatrixEvent[], room: Room) {
|
||||||
|
super(res, room);
|
||||||
|
this.zip = new JSZip();
|
||||||
|
this.avatars = new Map<string, boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected wrapHTML(content: string, room: Room) {
|
||||||
|
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">
|
||||||
|
<span class="mx_BaseAvatar" role="presentation"
|
||||||
|
><span
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
aria-hidden="true"
|
||||||
|
style="
|
||||||
|
font-size: 20.8px;
|
||||||
|
width: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
"
|
||||||
|
>G</span
|
||||||
|
><img
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
style="width: 32px; height: 32px"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx_RoomHeader_name">
|
||||||
|
<div
|
||||||
|
dir="auto"
|
||||||
|
class="mx_RoomHeader_nametext"
|
||||||
|
title="${room.name}"
|
||||||
|
>
|
||||||
|
${room.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx_RoomHeader_topic" dir="auto"></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"
|
||||||
|
>
|
||||||
|
${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>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// will be used in the future
|
||||||
|
protected async getMediaBlob(event: MatrixEvent) {
|
||||||
|
let blob: 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 = await image.blob();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error decrypting media");
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getDateSeparator(event: MatrixEvent) {
|
||||||
|
const ts = event.getTs();
|
||||||
|
const dateSeparator = <li key={ts}><DateSeparator isExporting={true} key={ts} ts={ts} /></li>;
|
||||||
|
return renderToStaticMarkup(dateSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _wantsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent) {
|
||||||
|
if (prevEvent == null) return true;
|
||||||
|
return wantsDateSeparator(prevEvent.getDate(), event.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
|
||||||
|
const eventTile = <li id={mxEv.getId()}>
|
||||||
|
<EventTile
|
||||||
|
mxEvent={mxEv}
|
||||||
|
continuation={joined}
|
||||||
|
isRedacted={mxEv.isRedacted()}
|
||||||
|
replacingEventId={mxEv.replacingEventId()}
|
||||||
|
isExporting={true}
|
||||||
|
readReceipts={null}
|
||||||
|
readReceiptMap={null}
|
||||||
|
showUrlPreview={false}
|
||||||
|
checkUnmounting={() => false}
|
||||||
|
isTwelveHour={false}
|
||||||
|
last={false}
|
||||||
|
lastInSection={false}
|
||||||
|
permalinkCreator={new RoomPermalinkCreator(this.room)}
|
||||||
|
lastSuccessful={false}
|
||||||
|
isSelectedEvent={false}
|
||||||
|
getRelationsForEvent={null}
|
||||||
|
showReactions={false}
|
||||||
|
layout={Layout.Group}
|
||||||
|
enableFlair={false}
|
||||||
|
showReadReceipts={false}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
return renderToStaticMarkup(eventTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createHTML(events: MatrixEvent[], room: Room) {
|
||||||
|
let content = "";
|
||||||
|
let prevEvent = null;
|
||||||
|
for (const event of events) {
|
||||||
|
if (!haveTileForEvent(event)) continue;
|
||||||
|
|
||||||
|
content += this._wantsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : "";
|
||||||
|
|
||||||
|
if (event.getType() === "m.room.message") {
|
||||||
|
const shouldBeJoined = !this._wantsDateSeparator(event, prevEvent)
|
||||||
|
&& shouldFormContinuation(prevEvent, event);
|
||||||
|
const body = await this.createMessageBody(event, shouldBeJoined);
|
||||||
|
content += body;
|
||||||
|
}
|
||||||
|
prevEvent = event;
|
||||||
|
}
|
||||||
|
return this.wrapHTML(content, room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async export() {
|
||||||
|
const html = await this.createHTML(this.res, this.room);
|
||||||
|
this.zip.file("index.html", html);
|
||||||
|
this.zip.file("css/style.css", exportCSS);
|
||||||
|
this.zip.file("js/script.js", exportJS);
|
||||||
|
|
||||||
|
const filename = `matrix-export-${new Date().toISOString()}.zip`;
|
||||||
|
|
||||||
|
//Generate the zip file asynchronously
|
||||||
|
const blob = await this.zip.generateAsync({ type: "blob" });
|
||||||
|
|
||||||
|
//Support for firefox browser
|
||||||
|
streamSaver.WritableStream = ponyfill.WritableStream
|
||||||
|
//Create a writable stream to the directory
|
||||||
|
const fileStream = streamSaver.createWriteStream(filename, { size: blob.size });
|
||||||
|
const writer = fileStream.getWriter();
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const blobPiece = blob.slice(fPointer, fPointer + sliceSize);
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
const waiter = new Promise<void>((resolve, reject) => {
|
||||||
|
reader.onloadend = evt => {
|
||||||
|
const arrayBufferNew: any = evt.target.result;
|
||||||
|
const uint8ArrayNew = new Uint8Array(arrayBufferNew);
|
||||||
|
writer.write(uint8ArrayNew);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(blobPiece);
|
||||||
|
});
|
||||||
|
await waiter;
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,32 @@
|
||||||
|
export default `
|
||||||
|
function scrollToElement(replyId){
|
||||||
|
let el = document.getElementById(replyId);
|
||||||
|
if(!el) {
|
||||||
|
showToast("The message you're looking for wasn't exported");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
el.style.backgroundColor = '#f6f7f8';
|
||||||
|
el.style.transition = 'background-color 1s ease'
|
||||||
|
setTimeout(() => {
|
||||||
|
el.style.backgroundColor = "white"
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(text) {
|
||||||
|
let 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 => {
|
||||||
|
scrollToElement(event.target.getAttribute("scroll-to"));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`
|
Loading…
Reference in New Issue