mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Apply review suggestions
This commit is contained in:
parent
c62210b07c
commit
7207329c15
@ -35,7 +35,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_RadioButton input[type="radio"]:checked + div > div {
|
.mx_RadioButton input[type="radio"]:checked + div > div {
|
||||||
background: gray;
|
background: $greyed-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RadioButton input[type=radio]:checked + div {
|
.mx_RadioButton input[type=radio]:checked + div {
|
||||||
@ -52,8 +52,8 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
||||||
background: gray;
|
background: $greyed-fg-color;
|
||||||
border-color: gray;
|
border-color: $greyed-fg-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +312,10 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
|||||||
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
||||||
} else if (ev.getType() == "m.sticker") {
|
} else if (ev.getType() == "m.sticker") {
|
||||||
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
|
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
|
||||||
} else message = senderDisplayName + ': ' + message;
|
} else {
|
||||||
|
// in this case, parse it as a plain text message
|
||||||
|
message = senderDisplayName + ': ' + message;
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,6 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||||||
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
||||||
viewUserOnClick?: boolean;
|
viewUserOnClick?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
forExport?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@ -90,8 +89,7 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
let { member, fallbackUserId, onClick, viewUserOnClick, ...otherProps } = this.props;
|
||||||
let { member, fallbackUserId, onClick, viewUserOnClick, forExport, ...otherProps } = this.props;
|
|
||||||
const userId = member ? member.userId : fallbackUserId;
|
const userId = member ? member.userId : fallbackUserId;
|
||||||
|
|
||||||
if (viewUserOnClick) {
|
if (viewUserOnClick) {
|
||||||
|
@ -29,13 +29,15 @@ import {
|
|||||||
textForFormat,
|
textForFormat,
|
||||||
textForType,
|
textForType,
|
||||||
} from "../../../utils/exportUtils/exportUtils";
|
} from "../../../utils/exportUtils/exportUtils";
|
||||||
import { IFieldState, IValidationResult } from "../elements/Validation";
|
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||||
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
|
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
|
||||||
import JSONExporter from "../../../utils/exportUtils/JSONExport";
|
import JSONExporter from "../../../utils/exportUtils/JSONExport";
|
||||||
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
|
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
|
||||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||||
import Exporter from "../../../utils/exportUtils/Exporter";
|
import Exporter from "../../../utils/exportUtils/Exporter";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import InfoDialog from "./InfoDialog";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@ -126,67 +128,85 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
|
|||||||
await startExport();
|
await startExport();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onValidateSize = async ({
|
const validateSize = withValidation({
|
||||||
value,
|
rules: [
|
||||||
}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
|
{
|
||||||
const parsedSize = parseFloat(value);
|
key: "required",
|
||||||
const min = 1;
|
test({ value, allowEmpty }) {
|
||||||
const max = 2000;
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 10 ** 8;
|
||||||
|
return _t("Enter a number between %(min)s and %(max)s", {
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
key: "number",
|
||||||
|
test: ({ value }) => {
|
||||||
|
const parsedSize = parseFloat(value);
|
||||||
|
const min = 1;
|
||||||
|
const max = 2000;
|
||||||
|
return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
|
||||||
|
},
|
||||||
|
invalid: () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 2000;
|
||||||
|
return _t(
|
||||||
|
"Size can only be a number between %(min)s MB and %(max)s MB",
|
||||||
|
{ min, max },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
if (isNaN(parsedSize)) {
|
const onValidateSize = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
return { valid: false, feedback: _t("Size must be a number") };
|
const result = await validateSize(fieldState);
|
||||||
}
|
return result;
|
||||||
|
|
||||||
if (min > parsedSize || parsedSize > max) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
feedback: _t(
|
|
||||||
"Size can only be between %(min)s MB and %(max)s MB",
|
|
||||||
{ min, max },
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
feedback: _t("Enter size between %(min)s MB and %(max)s MB", {
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onValidateNumberOfMessages = async ({
|
const validateNumberOfMessages = withValidation({
|
||||||
value,
|
rules: [
|
||||||
}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
|
{
|
||||||
const parsedSize = parseFloat(value);
|
key: "required",
|
||||||
const min = 1;
|
test({ value, allowEmpty }) {
|
||||||
const max = 10 ** 8;
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 10 ** 8;
|
||||||
|
return _t("Enter a number between %(min)s and %(max)s", {
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
key: "number",
|
||||||
|
test: ({ value }) => {
|
||||||
|
const parsedSize = parseFloat(value);
|
||||||
|
const min = 1;
|
||||||
|
const max = 10 ** 8;
|
||||||
|
if (isNaN(parsedSize)) return false;
|
||||||
|
return !(min > parsedSize || parsedSize > max);
|
||||||
|
},
|
||||||
|
invalid: () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 10 ** 8;
|
||||||
|
return _t(
|
||||||
|
"Number of messages can only be a number between %(min)s and %(max)s",
|
||||||
|
{ min, max },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
if (isNaN(parsedSize)) {
|
const onValidateNumberOfMessages = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
return {
|
const result = await validateNumberOfMessages(fieldState);
|
||||||
valid: false,
|
return result;
|
||||||
feedback: _t("Number of messages must be a number"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (min > parsedSize || parsedSize > max) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
feedback: _t(
|
|
||||||
"Number of messages can only be between %(min)s and %(max)s",
|
|
||||||
{ min, max },
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
feedback: _t("Enter a number between %(min)s and %(max)s", {
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCancel = async () => {
|
const onCancel = async () => {
|
||||||
@ -236,42 +256,20 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
|
|||||||
|
|
||||||
if (exportCancelled) {
|
if (exportCancelled) {
|
||||||
// Display successful cancellation message
|
// Display successful cancellation message
|
||||||
return (
|
Modal.createTrackedDialog("Export Cancelled", "", InfoDialog, {
|
||||||
<BaseDialog
|
title: _t("Export Cancelled"),
|
||||||
title={_t("Export Cancelled")}
|
description: <p>{ _t("The export was cancelled successfully") }</p>,
|
||||||
className="mx_ExportDialog"
|
hasCloseButton: true,
|
||||||
contentId="mx_Dialog_content"
|
});
|
||||||
onFinished={onFinished}
|
return null;
|
||||||
fixedWidth={true}
|
|
||||||
>
|
|
||||||
<p>{ _t("The export was cancelled successfully") }</p>
|
|
||||||
|
|
||||||
<DialogButtons
|
|
||||||
primaryButton={_t("Okay")}
|
|
||||||
hasCancel={false}
|
|
||||||
onPrimaryButtonClick={onFinished}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
} else if (exportSuccessful) {
|
} else if (exportSuccessful) {
|
||||||
// Display successful export message
|
// Display successful export message
|
||||||
return (
|
Modal.createTrackedDialog("Export Successful", "", InfoDialog, {
|
||||||
<BaseDialog
|
title: _t("Export Successful"),
|
||||||
title={_t("Export Successful")}
|
description: <p>{ _t("Your messages were successfully exported") }</p>,
|
||||||
className="mx_ExportDialog"
|
hasCloseButton: true,
|
||||||
contentId="mx_Dialog_content"
|
});
|
||||||
onFinished={onFinished}
|
return null;
|
||||||
fixedWidth={true}
|
|
||||||
>
|
|
||||||
<p>{ _t("Your messages were successfully exported") }</p>
|
|
||||||
|
|
||||||
<DialogButtons
|
|
||||||
primaryButton={_t("Okay")}
|
|
||||||
hasCancel={false}
|
|
||||||
onPrimaryButtonClick={onFinished}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
} else if (displayCancel) {
|
} else if (displayCancel) {
|
||||||
// Display cancel warning
|
// Display cancel warning
|
||||||
return (
|
return (
|
||||||
|
@ -355,10 +355,10 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
|||||||
} else if (this.props.forExport) {
|
} else if (this.props.forExport) {
|
||||||
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
|
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
|
||||||
header = <p className="mx_ReplyThread_Export">
|
header = <p className="mx_ReplyThread_Export">
|
||||||
{ _t("In reply to <messageLink/>",
|
{ _t("In reply to <a>this message</a>",
|
||||||
{},
|
{},
|
||||||
{ messageLink: () => (
|
{ a: (sub) => (
|
||||||
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { _t("this message") } </a>
|
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
callState: this.props.callEventGrouper?.state,
|
callState: this.props.callEventGrouper.state,
|
||||||
silenced: false,
|
silenced: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -210,7 +210,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||||||
render() {
|
render() {
|
||||||
const event = this.props.mxEvent;
|
const event = this.props.mxEvent;
|
||||||
const sender = event.sender ? event.sender.name : event.getSender();
|
const sender = event.sender ? event.sender.name : event.getSender();
|
||||||
const isVoice = this.props.callEventGrouper?.isVoice;
|
const isVoice = this.props.callEventGrouper.isVoice;
|
||||||
const callType = isVoice ? _t("Voice call") : _t("Video call");
|
const callType = isVoice ? _t("Voice call") : _t("Video call");
|
||||||
const callState = this.state.callState;
|
const callState = this.state.callState;
|
||||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||||
|
@ -201,11 +201,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||||||
if (this.props.showGenericPlaceholder) {
|
if (this.props.showGenericPlaceholder) {
|
||||||
placeholder = (
|
placeholder = (
|
||||||
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
|
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
|
||||||
<span className="mx_MFileBody_info_icon">
|
<span className="mx_MFileBody_info_icon" />
|
||||||
{ this.props.forExport ?
|
|
||||||
<img alt="Attachment" className="mx_export_attach_icon" src="icons/attach.svg" />
|
|
||||||
: null }
|
|
||||||
</span>
|
|
||||||
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
|
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
|
||||||
<span className="mx_MFileBody_info_filename">
|
<span className="mx_MFileBody_info_filename">
|
||||||
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }
|
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }
|
||||||
|
@ -39,7 +39,6 @@ const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent, forExport },
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_RedactedBody" ref={ref} title={titleText}>
|
<span className="mx_RedactedBody" ref={ref} title={titleText}>
|
||||||
{ forExport ? <img alt={_t("Redacted")} className="mx_export_trash_icon" src="icons/trash.svg" /> : null }
|
|
||||||
{ text }
|
{ text }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -725,6 +725,7 @@
|
|||||||
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
|
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
|
||||||
"Share your public space": "Share your public space",
|
"Share your public space": "Share your public space",
|
||||||
"Unknown App": "Unknown App",
|
"Unknown App": "Unknown App",
|
||||||
|
"Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?",
|
||||||
"HTML": "HTML",
|
"HTML": "HTML",
|
||||||
"JSON": "JSON",
|
"JSON": "JSON",
|
||||||
"Plain Text": "Plain Text",
|
"Plain Text": "Plain Text",
|
||||||
@ -1972,7 +1973,6 @@
|
|||||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
||||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||||
"Message deleted on %(date)s": "Message deleted on %(date)s",
|
"Message deleted on %(date)s": "Message deleted on %(date)s",
|
||||||
"Redacted": "Redacted",
|
|
||||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||||
@ -2122,8 +2122,7 @@
|
|||||||
"QR Code": "QR Code",
|
"QR Code": "QR Code",
|
||||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
||||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||||
"In reply to <messageLink/>": "In reply to <messageLink/>",
|
"In reply to <a>this message</a>": "In reply to <a>this message</a>",
|
||||||
"this message": "this message",
|
|
||||||
"Room address": "Room address",
|
"Room address": "Room address",
|
||||||
"e.g. my-room": "e.g. my-room",
|
"e.g. my-room": "e.g. my-room",
|
||||||
"Some characters not allowed": "Some characters not allowed",
|
"Some characters not allowed": "Some characters not allowed",
|
||||||
@ -2329,16 +2328,13 @@
|
|||||||
"There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
|
"There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
|
||||||
"Update community": "Update community",
|
"Update community": "Update community",
|
||||||
"An error has occurred.": "An error has occurred.",
|
"An error has occurred.": "An error has occurred.",
|
||||||
"Size can only be between %(min)s MB and %(max)s MB": "Size can only be between %(min)s MB and %(max)s MB",
|
|
||||||
"Enter size between %(min)s MB and %(max)s MB": "Enter size between %(min)s MB and %(max)s MB",
|
|
||||||
"Number of messages must be a number": "Number of messages must be a number",
|
|
||||||
"Number of messages can only be between %(min)s and %(max)s": "Number of messages can only be between %(min)s and %(max)s",
|
|
||||||
"Enter a number between %(min)s and %(max)s": "Enter a number between %(min)s and %(max)s",
|
"Enter a number between %(min)s and %(max)s": "Enter a number between %(min)s and %(max)s",
|
||||||
|
"Size can only be a number between %(min)s MB and %(max)s MB": "Size can only be a number between %(min)s MB and %(max)s MB",
|
||||||
|
"Number of messages can only be a number between %(min)s and %(max)s": "Number of messages can only be a number between %(min)s and %(max)s",
|
||||||
"Number of messages": "Number of messages",
|
"Number of messages": "Number of messages",
|
||||||
"MB": "MB",
|
"MB": "MB",
|
||||||
"Export Cancelled": "Export Cancelled",
|
"Export Cancelled": "Export Cancelled",
|
||||||
"The export was cancelled successfully": "The export was cancelled successfully",
|
"The export was cancelled successfully": "The export was cancelled successfully",
|
||||||
"Okay": "Okay",
|
|
||||||
"Export Successful": "Export Successful",
|
"Export Successful": "Export Successful",
|
||||||
"Your messages were successfully exported": "Your messages were successfully exported",
|
"Your messages were successfully exported": "Your messages were successfully exported",
|
||||||
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Are you sure you want to stop exporting your data? If you do, you'll need to start over.",
|
"Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "Are you sure you want to stop exporting your data? If you do, you'll need to start over.",
|
||||||
|
@ -25,6 +25,7 @@ import { Direction, MatrixClient } from "matrix-js-sdk";
|
|||||||
import { MutableRefObject } from "react";
|
import { MutableRefObject } from "react";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
|
||||||
type BlobFile = {
|
type BlobFile = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -54,7 +55,7 @@ export default abstract class Exporter {
|
|||||||
|
|
||||||
protected onBeforeUnload(e: BeforeUnloadEvent): string {
|
protected onBeforeUnload(e: BeforeUnloadEvent): string {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return e.returnValue = "Are you sure you want to exit during this export?";
|
return e.returnValue = _t("Are you sure you want to exit during this export?");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateProgress(progress: string, log = true, show = true): void {
|
protected updateProgress(progress: string, log = true, show = true): void {
|
||||||
@ -70,7 +71,7 @@ export default abstract class Exporter {
|
|||||||
this.files.push(file);
|
this.files.push(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async downloadZIP(): Promise<string | null> {
|
protected async downloadZIP(): Promise<string | void> {
|
||||||
const filename = `matrix-export-${formatFullDateNoDay(new Date())}.zip`;
|
const filename = `matrix-export-${formatFullDateNoDay(new Date())}.zip`;
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
@ -31,7 +31,6 @@ import EventTile, { haveTileForEvent } from "../../components/views/rooms/EventT
|
|||||||
import DateSeparator from "../../components/views/messages/DateSeparator";
|
import DateSeparator from "../../components/views/messages/DateSeparator";
|
||||||
import BaseAvatar from "../../components/views/avatars/BaseAvatar";
|
import BaseAvatar from "../../components/views/avatars/BaseAvatar";
|
||||||
import exportJS from "!!raw-loader!./exportJS";
|
import exportJS from "!!raw-loader!./exportJS";
|
||||||
import exportIcons from "./exportIcons";
|
|
||||||
import { ExportType } from "./exportUtils";
|
import { ExportType } from "./exportUtils";
|
||||||
import { IExportOptions } from "./exportUtils";
|
import { IExportOptions } from "./exportUtils";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
@ -294,6 +293,7 @@ export default class HTMLExporter extends Exporter {
|
|||||||
const mxc = mxEv.getContent().url || mxEv.getContent().file?.url;
|
const mxc = mxEv.getContent().url || mxEv.getContent().file?.url;
|
||||||
eventTileMarkup = eventTileMarkup.split(mxc).join(filePath);
|
eventTileMarkup = eventTileMarkup.split(mxc).join(filePath);
|
||||||
}
|
}
|
||||||
|
eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, '');
|
||||||
if (hasAvatar) {
|
if (hasAvatar) {
|
||||||
eventTileMarkup = eventTileMarkup.replace(
|
eventTileMarkup = eventTileMarkup.replace(
|
||||||
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, '&'),
|
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, '&'),
|
||||||
@ -406,10 +406,6 @@ export default class HTMLExporter extends Exporter {
|
|||||||
this.addFile("css/style.css", new Blob([exportCSS]));
|
this.addFile("css/style.css", new Blob([exportCSS]));
|
||||||
this.addFile("js/script.js", new Blob([exportJS]));
|
this.addFile("js/script.js", new Blob([exportJS]));
|
||||||
|
|
||||||
for (const iconName in exportIcons) {
|
|
||||||
this.addFile(`icons/${iconName}`, new Blob([exportIcons[iconName]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.downloadZIP();
|
await this.downloadZIP();
|
||||||
|
|
||||||
const exportEnd = performance.now();
|
const exportEnd = performance.now();
|
||||||
|
@ -16,33 +16,31 @@ limitations under the License.
|
|||||||
|
|
||||||
/* eslint-disable max-len, camelcase */
|
/* eslint-disable max-len, camelcase */
|
||||||
|
|
||||||
declare const __webpack_hash__: string;
|
|
||||||
|
|
||||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||||
|
|
||||||
const getExportCSS = async (): Promise<string> => {
|
const getExportCSS = async (): Promise<string> => {
|
||||||
const theme = new ThemeWatcher().getEffectiveTheme();
|
const theme = new ThemeWatcher().getEffectiveTheme();
|
||||||
const hash = __webpack_hash__;
|
const stylesheets: string[] = [];
|
||||||
|
document.querySelectorAll('link[rel="stylesheet"]').forEach((e: any) => {
|
||||||
const bundle = await fetch(`bundles/${hash}/bundle.css`);
|
if (e.href.endsWith("bundle.css") || e.href.endsWith(`theme-${theme}.css`)) {
|
||||||
const bundleCSS = await bundle.text();
|
stylesheets.push(e.href);
|
||||||
let themeCSS: string;
|
}
|
||||||
if (theme === 'light') {
|
});
|
||||||
const res = await fetch(`bundles/${hash}/theme-light.css`);
|
let CSS: string;
|
||||||
themeCSS = await res.text();
|
for (const stylesheet of stylesheets) {
|
||||||
} else {
|
const res = await fetch(stylesheet);
|
||||||
const res = await fetch(`bundles/${hash}/theme-dark.css`);
|
const innerText = await res.text();
|
||||||
themeCSS = await res.text();
|
CSS += innerText;
|
||||||
}
|
}
|
||||||
const fontFaceRegex = /@font-face {.*?}/sg;
|
const fontFaceRegex = /@font-face {.*?}/sg;
|
||||||
|
|
||||||
themeCSS = themeCSS.replace(fontFaceRegex, '');
|
CSS = CSS.replace(fontFaceRegex, '');
|
||||||
themeCSS = themeCSS.replace(
|
CSS = CSS.replace(
|
||||||
/font-family: Inter/g,
|
/font-family: Inter/g,
|
||||||
`font-family: -apple-system, BlinkMacSystemFont, avenir next,
|
`font-family: -apple-system, BlinkMacSystemFont, avenir next,
|
||||||
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`,
|
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`,
|
||||||
);
|
);
|
||||||
themeCSS = themeCSS.replace(
|
CSS = CSS.replace(
|
||||||
/font-family: Inconsolata/g,
|
/font-family: Inconsolata/g,
|
||||||
"font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
|
"font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
|
||||||
);
|
);
|
||||||
@ -149,13 +147,17 @@ const getExportCSS = async (): Promise<string> => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RedactedBody {
|
||||||
|
padding-left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return themeCSS + bundleCSS + customCSS;
|
return CSS + customCSS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getExportCSS;
|
export default getExportCSS;
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
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 trashSVG from "!!raw-loader!../../../res/img/element-icons/trashcan.svg";
|
|
||||||
import attachSVG from "!!raw-loader!../../../res/img/element-icons/room/composer/attach.svg";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"trash.svg": trashSVG,
|
|
||||||
"attach.svg": attachSVG,
|
|
||||||
};
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
|
import { IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
|
||||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||||
import { textForFormat, IExportOptions, ExportType } from "../../src/utils/exportUtils/exportUtils";
|
import { IExportOptions, ExportType, ExportFormat } from "../../src/utils/exportUtils/exportUtils";
|
||||||
import '../skinned-sdk';
|
import '../skinned-sdk';
|
||||||
import PlainTextExporter from "../../src/utils/exportUtils/PlainTextExport";
|
import PlainTextExporter from "../../src/utils/exportUtils/PlainTextExport";
|
||||||
import HTMLExporter from "../../src/utils/exportUtils/HtmlExport";
|
import HTMLExporter from "../../src/utils/exportUtils/HtmlExport";
|
||||||
@ -73,9 +73,60 @@ describe('export', function() {
|
|||||||
}
|
}
|
||||||
const mockRoom = createRoom();
|
const mockRoom = createRoom();
|
||||||
|
|
||||||
|
const ts0 = Date.now();
|
||||||
|
|
||||||
|
function mkRedactedEvent(i = 0) {
|
||||||
|
return new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: MY_USER_ID,
|
||||||
|
content: {},
|
||||||
|
unsigned: {
|
||||||
|
"age": 72,
|
||||||
|
"transaction_id": "m1212121212.23",
|
||||||
|
"redacted_because": {
|
||||||
|
"content": {},
|
||||||
|
"origin_server_ts": ts0 + i*1000,
|
||||||
|
"redacts": "$9999999999999999999999999999999999999999998",
|
||||||
|
"sender": "@me:here",
|
||||||
|
"type": "m.room.redaction",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 94,
|
||||||
|
"transaction_id": "m1111111111.1",
|
||||||
|
},
|
||||||
|
"event_id": "$9999999999999999999999999999999999999999998",
|
||||||
|
"room_id": mockRoom.roomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event_id: "$9999999999999999999999999999999999999999999",
|
||||||
|
room_id: mockRoom.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkFileEvent() {
|
||||||
|
return new MatrixEvent({
|
||||||
|
"content": {
|
||||||
|
"body": "index.html",
|
||||||
|
"info": {
|
||||||
|
"mimetype": "text/html",
|
||||||
|
"size": 31613,
|
||||||
|
},
|
||||||
|
"msgtype": "m.file",
|
||||||
|
"url": "mxc://test.org",
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1628872988364,
|
||||||
|
"sender": MY_USER_ID,
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 266,
|
||||||
|
"transaction_id": "m99999999.2",
|
||||||
|
},
|
||||||
|
"event_id": "$99999999999999999999",
|
||||||
|
"room_id": mockRoom.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function mkEvents() {
|
function mkEvents() {
|
||||||
const matrixEvents = [];
|
const matrixEvents = [];
|
||||||
const ts0 = Date.now();
|
|
||||||
let i: number;
|
let i: number;
|
||||||
// plain text
|
// plain text
|
||||||
for (i = 0; i < 10; i++) {
|
for (i = 0; i < 10; i++) {
|
||||||
@ -134,30 +185,7 @@ describe('export', function() {
|
|||||||
}));
|
}));
|
||||||
// redacted events
|
// redacted events
|
||||||
for (i = 0; i < 10; i++) {
|
for (i = 0; i < 10; i++) {
|
||||||
matrixEvents.push(new MatrixEvent({
|
matrixEvents.push(mkRedactedEvent(i));
|
||||||
type: "m.room.message",
|
|
||||||
sender: MY_USER_ID,
|
|
||||||
content: {},
|
|
||||||
unsigned: {
|
|
||||||
"age": 72,
|
|
||||||
"transaction_id": "m1212121212.23",
|
|
||||||
"redacted_because": {
|
|
||||||
"content": {},
|
|
||||||
"origin_server_ts": ts0 + i*1000,
|
|
||||||
"redacts": "$9999999999999999999999999999999999999999998",
|
|
||||||
"sender": "@me:here",
|
|
||||||
"type": "m.room.redaction",
|
|
||||||
"unsigned": {
|
|
||||||
"age": 94,
|
|
||||||
"transaction_id": "m1111111111.1",
|
|
||||||
},
|
|
||||||
"event_id": "$9999999999999999999999999999999999999999998",
|
|
||||||
"room_id": mockRoom.roomId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event_id: "$9999999999999999999999999999999999999999999",
|
|
||||||
room_id: mockRoom.roomId,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
return matrixEvents;
|
return matrixEvents;
|
||||||
}
|
}
|
||||||
@ -165,10 +193,22 @@ describe('export', function() {
|
|||||||
const events: MatrixEvent[] = mkEvents();
|
const events: MatrixEvent[] = mkEvents();
|
||||||
|
|
||||||
it('checks if the export format is valid', function() {
|
it('checks if the export format is valid', function() {
|
||||||
expect(textForFormat('HTML')).toBeTruthy();
|
function isValidFormat(format: string): boolean {
|
||||||
expect(textForFormat('JSON')).toBeTruthy();
|
const options: string[] = Object.values(ExportFormat);
|
||||||
expect(textForFormat('PLAIN_TEXT')).toBeTruthy();
|
return options.includes(format);
|
||||||
expect(() => textForFormat('PDF')).toThrowError("Unknown format");
|
}
|
||||||
|
expect(isValidFormat("Html")).toBeTruthy();
|
||||||
|
expect(isValidFormat("Json")).toBeTruthy();
|
||||||
|
expect(isValidFormat("PlainText")).toBeTruthy();
|
||||||
|
expect(isValidFormat("Pdf")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks if the icons' html corresponds to export regex", function() {
|
||||||
|
const exporter = new HTMLExporter(mockRoom, ExportType.Beginning, mockExportOptions, null);
|
||||||
|
const fileRegex = /<span class="mx_MFileBody_info_icon">.*?<\/span>/;
|
||||||
|
expect(fileRegex.test(
|
||||||
|
renderToString(exporter.getEventTile(mkFileEvent(), true))),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks if the export options are valid', function() {
|
it('checks if the export options are valid', function() {
|
||||||
|
Loading…
Reference in New Issue
Block a user