Apply review suggestions

This commit is contained in:
Jaiwanth 2021-08-13 23:44:07 +05:30
parent c62210b07c
commit 7207329c15
14 changed files with 201 additions and 195 deletions

View File

@ -35,7 +35,7 @@ limitations under the License.
}
.mx_RadioButton input[type="radio"]:checked + div > div {
background: gray;
background: $greyed-fg-color;
}
.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 {
background: gray;
border-color: gray;
background: $greyed-fg-color;
border-color: $greyed-fg-color;
}
}

View File

@ -312,7 +312,10 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
} else if (ev.getType() == "m.sticker") {
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;
};
}

View File

@ -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`
viewUserOnClick?: boolean;
title?: string;
forExport?: boolean;
}
interface IState {
@ -90,8 +89,7 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
}
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let { member, fallbackUserId, onClick, viewUserOnClick, forExport, ...otherProps } = this.props;
let { member, fallbackUserId, onClick, viewUserOnClick, ...otherProps } = this.props;
const userId = member ? member.userId : fallbackUserId;
if (viewUserOnClick) {

View File

@ -29,13 +29,15 @@ import {
textForFormat,
textForType,
} from "../../../utils/exportUtils/exportUtils";
import { IFieldState, IValidationResult } from "../elements/Validation";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
import JSONExporter from "../../../utils/exportUtils/JSONExport";
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
import { useStateCallback } from "../../../hooks/useStateCallback";
import Exporter from "../../../utils/exportUtils/Exporter";
import Spinner from "../elements/Spinner";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
interface IProps extends IDialogProps {
room: Room;
@ -126,67 +128,85 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
await startExport();
};
const onValidateSize = async ({
value,
}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
const parsedSize = parseFloat(value);
const min = 1;
const max = 2000;
const validateSize = withValidation({
rules: [
{
key: "required",
test({ value, allowEmpty }) {
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)) {
return { valid: false, feedback: _t("Size must be a number") };
}
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 onValidateSize = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await validateSize(fieldState);
return result;
};
const onValidateNumberOfMessages = async ({
value,
}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
const parsedSize = parseFloat(value);
const min = 1;
const max = 10 ** 8;
const validateNumberOfMessages = withValidation({
rules: [
{
key: "required",
test({ value, allowEmpty }) {
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)) {
return {
valid: false,
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 onValidateNumberOfMessages = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await validateNumberOfMessages(fieldState);
return result;
};
const onCancel = async () => {
@ -236,42 +256,20 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
if (exportCancelled) {
// Display successful cancellation message
return (
<BaseDialog
title={_t("Export Cancelled")}
className="mx_ExportDialog"
contentId="mx_Dialog_content"
onFinished={onFinished}
fixedWidth={true}
>
<p>{ _t("The export was cancelled successfully") }</p>
<DialogButtons
primaryButton={_t("Okay")}
hasCancel={false}
onPrimaryButtonClick={onFinished}
/>
</BaseDialog>
);
Modal.createTrackedDialog("Export Cancelled", "", InfoDialog, {
title: _t("Export Cancelled"),
description: <p>{ _t("The export was cancelled successfully") }</p>,
hasCloseButton: true,
});
return null;
} else if (exportSuccessful) {
// Display successful export message
return (
<BaseDialog
title={_t("Export Successful")}
className="mx_ExportDialog"
contentId="mx_Dialog_content"
onFinished={onFinished}
fixedWidth={true}
>
<p>{ _t("Your messages were successfully exported") }</p>
<DialogButtons
primaryButton={_t("Okay")}
hasCancel={false}
onPrimaryButtonClick={onFinished}
/>
</BaseDialog>
);
Modal.createTrackedDialog("Export Successful", "", InfoDialog, {
title: _t("Export Successful"),
description: <p>{ _t("Your messages were successfully exported") }</p>,
hasCloseButton: true,
});
return null;
} else if (displayCancel) {
// Display cancel warning
return (

View File

@ -355,10 +355,10 @@ export default class ReplyThread extends React.Component<IProps, IState> {
} else if (this.props.forExport) {
const eventId = ReplyThread.getParentEventId(this.props.parentEv);
header = <p className="mx_ReplyThread_Export">
{ _t("In reply to <messageLink/>",
{ _t("In reply to <a>this message</a>",
{},
{ messageLink: () => (
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { _t("this message") } </a>
{ a: (sub) => (
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
),
})
}

View File

@ -47,7 +47,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
super(props);
this.state = {
callState: this.props.callEventGrouper?.state,
callState: this.props.callEventGrouper.state,
silenced: false,
};
}
@ -210,7 +210,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
render() {
const event = this.props.mxEvent;
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 callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;

View File

@ -201,11 +201,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
if (this.props.showGenericPlaceholder) {
placeholder = (
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
<span className="mx_MFileBody_info_icon">
{ this.props.forExport ?
<img alt="Attachment" className="mx_export_attach_icon" src="icons/attach.svg" />
: null }
</span>
<span className="mx_MFileBody_info_icon" />
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }

View File

@ -39,7 +39,6 @@ const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent, forExport },
return (
<span className="mx_RedactedBody" ref={ref} title={titleText}>
{ forExport ? <img alt={_t("Redacted")} className="mx_export_trash_icon" src="icons/trash.svg" /> : null }
{ text }
</span>
);

View File

@ -725,6 +725,7 @@
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"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",
"JSON": "JSON",
"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 %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
"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 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/>",
@ -2122,8 +2122,7 @@
"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.",
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
"In reply to <messageLink/>": "In reply to <messageLink/>",
"this message": "this message",
"In reply to <a>this message</a>": "In reply to <a>this message</a>",
"Room address": "Room address",
"e.g. my-room": "e.g. my-room",
"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.",
"Update community": "Update community",
"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",
"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",
"MB": "MB",
"Export Cancelled": "Export Cancelled",
"The export was cancelled successfully": "The export was cancelled successfully",
"Okay": "Okay",
"Export Successful": "Export Successful",
"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.",

View File

@ -25,6 +25,7 @@ import { Direction, MatrixClient } from "matrix-js-sdk";
import { MutableRefObject } from "react";
import JSZip from "jszip";
import { saveAs } from "file-saver";
import { _t } from "../../languageHandler";
type BlobFile = {
name: string;
@ -54,7 +55,7 @@ export default abstract class Exporter {
protected onBeforeUnload(e: BeforeUnloadEvent): string {
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 {
@ -70,7 +71,7 @@ export default abstract class Exporter {
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 zip = new JSZip();

View File

@ -31,7 +31,6 @@ import EventTile, { haveTileForEvent } from "../../components/views/rooms/EventT
import DateSeparator from "../../components/views/messages/DateSeparator";
import BaseAvatar from "../../components/views/avatars/BaseAvatar";
import exportJS from "!!raw-loader!./exportJS";
import exportIcons from "./exportIcons";
import { ExportType } from "./exportUtils";
import { IExportOptions } from "./exportUtils";
import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -294,6 +293,7 @@ export default class HTMLExporter extends Exporter {
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, '&amp;'),
@ -406,10 +406,6 @@ export default class HTMLExporter extends Exporter {
this.addFile("css/style.css", new Blob([exportCSS]));
this.addFile("js/script.js", new Blob([exportJS]));
for (const iconName in exportIcons) {
this.addFile(`icons/${iconName}`, new Blob([exportIcons[iconName]]));
}
await this.downloadZIP();
const exportEnd = performance.now();

View File

@ -16,33 +16,31 @@ limitations under the License.
/* eslint-disable max-len, camelcase */
declare const __webpack_hash__: string;
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
const getExportCSS = async (): Promise<string> => {
const theme = new ThemeWatcher().getEffectiveTheme();
const hash = __webpack_hash__;
const bundle = await fetch(`bundles/${hash}/bundle.css`);
const bundleCSS = await bundle.text();
let themeCSS: string;
if (theme === 'light') {
const res = await fetch(`bundles/${hash}/theme-light.css`);
themeCSS = await res.text();
} else {
const res = await fetch(`bundles/${hash}/theme-dark.css`);
themeCSS = await res.text();
const stylesheets: string[] = [];
document.querySelectorAll('link[rel="stylesheet"]').forEach((e: any) => {
if (e.href.endsWith("bundle.css") || e.href.endsWith(`theme-${theme}.css`)) {
stylesheets.push(e.href);
}
});
let CSS: string;
for (const stylesheet of stylesheets) {
const res = await fetch(stylesheet);
const innerText = await res.text();
CSS += innerText;
}
const fontFaceRegex = /@font-face {.*?}/sg;
themeCSS = themeCSS.replace(fontFaceRegex, '');
themeCSS = themeCSS.replace(
CSS = CSS.replace(fontFaceRegex, '');
CSS = CSS.replace(
/font-family: Inter/g,
`font-family: -apple-system, BlinkMacSystemFont, avenir next,
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`,
);
themeCSS = themeCSS.replace(
CSS = CSS.replace(
/font-family: Inconsolata/g,
"font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
);
@ -148,6 +146,10 @@ const getExportCSS = async (): Promise<string> => {
top: 1px;
left: 0;
}
.mx_RedactedBody {
padding-left: unset;
}
img {
white-space: nowrap;
@ -155,7 +157,7 @@ const getExportCSS = async (): Promise<string> => {
}
`;
return themeCSS + bundleCSS + customCSS;
return CSS + customCSS;
};
export default getExportCSS;

View File

@ -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,
};

View File

@ -16,7 +16,7 @@ limitations under the License.
import { IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
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 PlainTextExporter from "../../src/utils/exportUtils/PlainTextExport";
import HTMLExporter from "../../src/utils/exportUtils/HtmlExport";
@ -73,9 +73,60 @@ describe('export', function() {
}
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() {
const matrixEvents = [];
const ts0 = Date.now();
let i: number;
// plain text
for (i = 0; i < 10; i++) {
@ -134,30 +185,7 @@ describe('export', function() {
}));
// redacted events
for (i = 0; i < 10; i++) {
matrixEvents.push(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,
}));
matrixEvents.push(mkRedactedEvent(i));
}
return matrixEvents;
}
@ -165,10 +193,22 @@ describe('export', function() {
const events: MatrixEvent[] = mkEvents();
it('checks if the export format is valid', function() {
expect(textForFormat('HTML')).toBeTruthy();
expect(textForFormat('JSON')).toBeTruthy();
expect(textForFormat('PLAIN_TEXT')).toBeTruthy();
expect(() => textForFormat('PDF')).toThrowError("Unknown format");
function isValidFormat(format: string): boolean {
const options: string[] = Object.values(ExportFormat);
return options.includes(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() {