{ body }
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 1568e06720..c9ea1143de 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -73,6 +73,8 @@ import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore";
+import { logger } from "matrix-js-sdk/src/logger";
+
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -775,7 +777,7 @@ export default class InviteDialog extends React.PureComponent>;
+ source: string;
+ continuation: () => void;
+}
+
+const KeySignatureUploadFailedDialog: React.FC = ({
failures,
source,
continuation,
onFinished,
-}) {
+}) => {
const RETRIES = 2;
- const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- const Spinner = sdk.getComponent('elements.Spinner');
const [retry, setRetry] = useState(RETRIES);
const [cancelled, setCancelled] = useState(false);
const [retrying, setRetrying] = useState(false);
@@ -107,4 +116,6 @@ export default function KeySignatureUploadFailedDialog({
{ body }
);
-}
+};
+
+export default KeySignatureUploadFailedDialog;
diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.js b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx
similarity index 89%
rename from src/components/views/dialogs/LazyLoadingDisabledDialog.js
rename to src/components/views/dialogs/LazyLoadingDisabledDialog.tsx
index e43cb28a22..ec30123436 100644
--- a/src/components/views/dialogs/LazyLoadingDisabledDialog.js
+++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx
@@ -19,8 +19,13 @@ import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
+import { IDialogProps } from "./IDialogProps";
-export default (props) => {
+interface IProps extends IDialogProps {
+ host: string;
+}
+
+const LazyLoadingDisabledDialog: React.FC = (props) => {
const brand = SdkConfig.get().brand;
const description1 = _t(
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
@@ -49,3 +54,5 @@ export default (props) => {
onFinished={props.onFinished}
/>);
};
+
+export default LazyLoadingDisabledDialog;
diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.js b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx
similarity index 87%
rename from src/components/views/dialogs/LazyLoadingResyncDialog.js
rename to src/components/views/dialogs/LazyLoadingResyncDialog.tsx
index a5db15ebbe..e6a505511c 100644
--- a/src/components/views/dialogs/LazyLoadingResyncDialog.js
+++ b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx
@@ -19,8 +19,11 @@ import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
+import { IDialogProps } from "./IDialogProps";
-export default (props) => {
+interface IProps extends IDialogProps {}
+
+const LazyLoadingResyncDialog: React.FC = (props) => {
const brand = SdkConfig.get().brand;
const description =
_t(
@@ -38,3 +41,5 @@ export default (props) => {
onFinished={props.onFinished}
/>);
};
+
+export default LazyLoadingResyncDialog;
diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx
index d26c0a8b6a..a6f2f1e5c4 100644
--- a/src/components/views/dialogs/LeaveSpaceDialog.tsx
+++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx
@@ -97,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
definitions={[
{
value: RoomsToLeave.None,
- label: _t("Don't leave any"),
+ label: _t("Don't leave any rooms"),
}, {
value: RoomsToLeave.All,
- label: _t("Leave all rooms and spaces"),
+ label: _t("Leave all rooms"),
}, {
value: RoomsToLeave.Specific,
- label: _t("Leave specific rooms and spaces"),
+ label: _t("Leave some rooms"),
},
]}
/>
@@ -171,11 +171,13 @@ const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => {
>
- { _t("Are you sure you want to leave ?", {}, {
+ { _t("You are about to leave .", {}, {
spaceName: () => { space.name },
}) }
{ rejoinWarning }
+ { rejoinWarning && (<> >) }
+ { spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
{ spaceChildren.length > 0 && void;
}
@@ -68,7 +70,7 @@ export default class LogoutDialog extends React.Component {
backupInfo,
});
} catch (e) {
- console.log("Unable to fetch key backup status", e);
+ logger.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
similarity index 84%
rename from src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
rename to src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
index 4387108fac..88419d26b8 100644
--- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
+++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
@@ -19,37 +19,31 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import QuestionDialog from "./QuestionDialog";
+import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
+import { IDialogProps } from "./IDialogProps";
+
+interface IProps extends IDialogProps {
+ userId: string;
+ device: DeviceInfo;
+}
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
-export default class ManualDeviceKeyVerificationDialog extends React.Component {
- static propTypes = {
- userId: PropTypes.string.isRequired,
- device: PropTypes.object.isRequired,
- onFinished: PropTypes.func.isRequired,
- };
-
- _onCancelClick = () => {
- this.props.onFinished(false);
- }
-
- _onLegacyFinished = (confirm) => {
+export default class ManualDeviceKeyVerificationDialog extends React.Component {
+ private onLegacyFinished = (confirm: boolean): void => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
- }
-
- render() {
- const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ };
+ public render(): JSX.Element {
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:");
@@ -81,7 +75,7 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component {
title={_t("Verify session")}
description={body}
button={_t("Verify session")}
- onFinished={this._onLegacyFinished}
+ onFinished={this.onLegacyFinished}
/>
);
}
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
similarity index 81%
rename from src/components/views/dialogs/MessageEditHistoryDialog.js
rename to src/components/views/dialogs/MessageEditHistoryDialog.tsx
index 6fce8aecd4..7753eba199 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -15,21 +15,39 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
-import * as sdk from "../../../index";
import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import BaseDialog from "./BaseDialog";
+import ScrollPanel from "../../structures/ScrollPanel";
+import Spinner from "../elements/Spinner";
+import EditHistoryMessage from "../messages/EditHistoryMessage";
+import DateSeparator from "../messages/DateSeparator";
+import { IDialogProps } from "./IDialogProps";
+import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
+import { defer } from "matrix-js-sdk/src/utils";
+
+interface IProps extends IDialogProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ originalEvent: MatrixEvent;
+ error: {
+ errcode: string;
+ };
+ events: MatrixEvent[];
+ nextBatch: string;
+ isLoading: boolean;
+ isTwelveHour: boolean;
+}
@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
-export default class MessageEditHistoryDialog extends React.PureComponent {
- static propTypes = {
- mxEvent: PropTypes.object.isRequired,
- };
-
- constructor(props) {
+export default class MessageEditHistoryDialog extends React.PureComponent {
+ constructor(props: IProps) {
super(props);
this.state = {
originalEvent: null,
@@ -41,7 +59,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
};
}
- loadMoreEdits = async (backwards) => {
+ private loadMoreEdits = async (backwards?: boolean): Promise => {
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
// bail out on backwards as we only paginate in one direction
return false;
@@ -50,13 +68,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId();
const client = MatrixClientPeg.get();
+
+ const { resolve, reject, promise } = defer();
let result;
- let resolve;
- let reject;
- const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
+
try {
result = await client.relations(
- roomId, eventId, "m.replace", "m.room.message", opts);
+ roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts);
} catch (error) {
// log if the server returned an error
if (error.errcode) {
@@ -67,7 +85,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
}
const newEvents = result.events;
- this._locallyRedactEventsIfNeeded(newEvents);
+ this.locallyRedactEventsIfNeeded(newEvents);
this.setState({
originalEvent: this.state.originalEvent || result.originalEvent,
events: this.state.events.concat(newEvents),
@@ -78,9 +96,9 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
resolve(hasMoreResults);
});
return promise;
- }
+ };
- _locallyRedactEventsIfNeeded(newEvents) {
+ private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void {
const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
@@ -95,13 +113,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
}
}
- componentDidMount() {
+ public componentDidMount(): void {
this.loadMoreEdits();
}
- _renderEdits() {
- const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ private renderEdits(): JSX.Element[] {
const nodes = [];
let lastEvent;
let allEvents = this.state.events;
@@ -128,7 +144,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
return nodes;
}
- render() {
+ public render(): JSX.Element {
let content;
if (this.state.error) {
const { error } = this.state;
@@ -149,20 +165,17 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
);
}
} else if (this.state.isLoading) {
- const Spinner = sdk.getComponent("elements.Spinner");
content = ;
} else {
- const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
content = (
-
{ this._renderEdits() }
+
{ this.renderEdits() }
);
}
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
{
+ public static defaultProps: Partial = {
title: "",
description: "",
extraButtons: null,
@@ -48,17 +49,19 @@ export default class QuestionDialog extends React.Component {
quitOnly: false,
};
- onOk = () => {
+ private onOk = (): void => {
this.props.onFinished(true);
};
- onCancel = () => {
+ private onCancel = (): void => {
this.props.onFinished(false);
};
- render() {
+ public render(): JSX.Element {
+ // Converting these to imports breaks wrench tests
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+
let primaryButtonClass = "";
if (this.props.danger) {
primaryButtonClass = "danger";
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx
similarity index 80%
rename from src/components/views/dialogs/SessionRestoreErrorDialog.js
rename to src/components/views/dialogs/SessionRestoreErrorDialog.tsx
index eeeadbbfe5..b36dbf548e 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.js
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx
@@ -17,27 +17,27 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import QuestionDialog from "./QuestionDialog";
+import BugReportDialog from "./BugReportDialog";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
+import { IDialogProps } from "./IDialogProps";
+
+interface IProps extends IDialogProps {
+ error: string;
+}
@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
-export default class SessionRestoreErrorDialog extends React.Component {
- static propTypes = {
- error: PropTypes.string.isRequired,
- onFinished: PropTypes.func.isRequired,
- };
-
- _sendBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
+export default class SessionRestoreErrorDialog extends React.Component {
+ private sendBugReport = (): void => {
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
};
- _onClearStorageClick = () => {
- const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ private onClearStorageClick = (): void => {
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"),
description:
@@ -48,19 +48,17 @@ export default class SessionRestoreErrorDialog extends React.Component {
});
};
- _onRefreshClick = () => {
+ private onRefreshClick = (): void => {
// Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful.
- window.location.reload(true);
+ window.location.reload();
};
- render() {
+ public render(): JSX.Element {
const brand = SdkConfig.get().brand;
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const clearStorageButton = (
-
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
similarity index 78%
rename from src/components/views/dialogs/TabbedIntegrationManagerDialog.js
rename to src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
index 8723d4a453..0f87b5c18d 100644
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
@@ -15,42 +15,47 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { Room } from "matrix-js-sdk/src/models/room";
-import * as sdk from '../../../index';
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import classNames from 'classnames';
import * as ScalarMessaging from "../../../ScalarMessaging";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
+import ScalarAuthClient from "../../../ScalarAuthClient";
+import AccessibleButton from "../elements/AccessibleButton";
+import IntegrationManager from "../settings/IntegrationManager";
+import { IDialogProps } from "./IDialogProps";
+
+interface IProps extends IDialogProps {
+ /**
+ * Optional room where the integration manager should be open to
+ */
+ room?: Room;
+
+ /**
+ * Optional screen to open on the integration manager
+ */
+ screen?: string;
+
+ /**
+ * Optional integration ID to open in the integration manager
+ */
+ integrationId?: string;
+}
+
+interface IState {
+ managers: IntegrationManagerInstance[];
+ busy: boolean;
+ currentIndex: number;
+ currentConnected: boolean;
+ currentLoading: boolean;
+ currentScalarClient: ScalarAuthClient;
+}
@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
-export default class TabbedIntegrationManagerDialog extends React.Component {
- static propTypes = {
- /**
- * Called with:
- * * success {bool} True if the user accepted any douments, false if cancelled
- * * agreedUrls {string[]} List of agreed URLs
- */
- onFinished: PropTypes.func.isRequired,
-
- /**
- * Optional room where the integration manager should be open to
- */
- room: PropTypes.instanceOf(Room),
-
- /**
- * Optional screen to open on the integration manager
- */
- screen: PropTypes.string,
-
- /**
- * Optional integration ID to open in the integration manager
- */
- integrationId: PropTypes.string,
- };
-
- constructor(props) {
+export default class TabbedIntegrationManagerDialog extends React.Component {
+ constructor(props: IProps) {
super(props);
this.state = {
@@ -63,11 +68,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
};
}
- componentDidMount() {
+ public componentDidMount(): void {
this.openManager(0, true);
}
- openManager = async (i, force = false) => {
+ private openManager = async (i: number, force = false): Promise => {
if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i];
@@ -120,8 +125,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}
};
- _renderTabs() {
- const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
+ private renderTabs(): JSX.Element[] {
return this.state.managers.map((m, i) => {
const classes = classNames({
'mx_TabbedIntegrationManagerDialog_tab': true,
@@ -140,8 +144,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
});
}
- _renderTab() {
- const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
+ public renderTab(): JSX.Element {
let uiUrl = null;
if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
@@ -151,7 +154,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
);
}
return ;
}
- render() {
+ public render(): JSX.Element {
return (
- { this._renderTabs() }
+ { this.renderTabs() }
- { this._renderTab() }
+ { this.renderTab() }
);
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.tsx
similarity index 67%
rename from src/components/views/dialogs/TextInputDialog.js
rename to src/components/views/dialogs/TextInputDialog.tsx
index 3d37c89424..7a5887f053 100644
--- a/src/components/views/dialogs/TextInputDialog.js
+++ b/src/components/views/dialogs/TextInputDialog.tsx
@@ -14,33 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
+import React, { ChangeEvent, createRef } from 'react';
import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { IFieldState, IValidationResult } from "../elements/Validation";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
+import { IDialogProps } from "./IDialogProps";
+
+interface IProps extends IDialogProps {
+ title?: string;
+ description?: React.ReactNode;
+ value?: string;
+ placeholder?: string;
+ button?: string;
+ busyMessage?: string; // pass _td string
+ focus?: boolean;
+ hasCancel?: boolean;
+ validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
+ fixedWidth?: boolean;
+}
+
+interface IState {
+ value: string;
+ busy: boolean;
+ valid: boolean;
+}
@replaceableComponent("views.dialogs.TextInputDialog")
-export default class TextInputDialog extends React.Component {
- static propTypes = {
- title: PropTypes.string,
- description: PropTypes.oneOfType([
- PropTypes.element,
- PropTypes.string,
- ]),
- value: PropTypes.string,
- placeholder: PropTypes.string,
- button: PropTypes.string,
- busyMessage: PropTypes.string, // pass _td string
- focus: PropTypes.bool,
- onFinished: PropTypes.func.isRequired,
- hasCancel: PropTypes.bool,
- validator: PropTypes.func, // result of withValidation
- fixedWidth: PropTypes.bool,
- };
+export default class TextInputDialog extends React.Component {
+ private field = createRef();
- static defaultProps = {
+ public static defaultProps = {
title: "",
value: "",
description: "",
@@ -49,11 +55,9 @@ export default class TextInputDialog extends React.Component {
hasCancel: true,
};
- constructor(props) {
+ constructor(props: IProps) {
super(props);
- this._field = createRef();
-
this.state = {
value: this.props.value,
busy: false,
@@ -61,23 +65,23 @@ export default class TextInputDialog extends React.Component {
};
}
- componentDidMount() {
+ public componentDidMount(): void {
if (this.props.focus) {
// Set the cursor at the end of the text input
// this._field.current.value = this.props.value;
- this._field.current.focus();
+ this.field.current.focus();
}
}
- onOk = async ev => {
+ private onOk = async (ev: React.FormEvent): Promise => {
ev.preventDefault();
if (this.props.validator) {
this.setState({ busy: true });
- await this._field.current.validate({ allowEmpty: false });
+ await this.field.current.validate({ allowEmpty: false });
- if (!this._field.current.state.valid) {
- this._field.current.focus();
- this._field.current.validate({ allowEmpty: false, focused: true });
+ if (!this.field.current.state.valid) {
+ this.field.current.focus();
+ this.field.current.validate({ allowEmpty: false, focused: true });
this.setState({ busy: false });
return;
}
@@ -85,17 +89,17 @@ export default class TextInputDialog extends React.Component {
this.props.onFinished(true, this.state.value);
};
- onCancel = () => {
+ private onCancel = (): void => {
this.props.onFinished(false);
};
- onChange = ev => {
+ private onChange = (ev: ChangeEvent): void => {
this.setState({
value: ev.target.value,
});
};
- onValidate = async fieldState => {
+ private onValidate = async (fieldState: IFieldState): Promise => {
const result = await this.props.validator(fieldState);
this.setState({
valid: result.valid,
@@ -103,9 +107,7 @@ export default class TextInputDialog extends React.Component {
return result;
};
- render() {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ public render(): JSX.Element {
return (
diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
index 8389757347..8c503e340d 100644
--- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx
+++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
@@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import E2EIcon from "../rooms/E2EIcon";
+import E2EIcon, { E2EState } from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
@@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={<>
-
+
{ _t("Not Trusted") }
>}
>
diff --git a/src/components/views/dialogs/UploadFailureDialog.js b/src/components/views/dialogs/UploadFailureDialog.tsx
similarity index 80%
rename from src/components/views/dialogs/UploadFailureDialog.js
rename to src/components/views/dialogs/UploadFailureDialog.tsx
index 224098f935..bb8d14e161 100644
--- a/src/components/views/dialogs/UploadFailureDialog.js
+++ b/src/components/views/dialogs/UploadFailureDialog.tsx
@@ -17,11 +17,18 @@ limitations under the License.
import filesize from 'filesize';
import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
+import { IDialogProps } from "./IDialogProps";
+
+interface IProps extends IDialogProps {
+ badFiles: File[];
+ totalFiles: number;
+ contentMessages: ContentMessages;
+}
/*
* Tells the user about files we know cannot be uploaded before we even try uploading
@@ -29,26 +36,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* the size of the file.
*/
@replaceableComponent("views.dialogs.UploadFailureDialog")
-export default class UploadFailureDialog extends React.Component {
- static propTypes = {
- badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
- totalFiles: PropTypes.number.isRequired,
- contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
- onFinished: PropTypes.func.isRequired,
- }
-
- _onCancelClick = () => {
+export default class UploadFailureDialog extends React.Component {
+ private onCancelClick = (): void => {
this.props.onFinished(false);
- }
+ };
- _onUploadClick = () => {
+ private onUploadClick = (): void => {
this.props.onFinished(true);
- }
-
- render() {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ };
+ public render(): JSX.Element {
let message;
let preview;
let buttons;
@@ -65,7 +62,7 @@ export default class UploadFailureDialog extends React.Component {
);
buttons = ;
} else if (this.props.totalFiles === this.props.badFiles.length) {
@@ -80,7 +77,7 @@ export default class UploadFailureDialog extends React.Component {
);
buttons = ;
} else {
@@ -96,17 +93,17 @@ export default class UploadFailureDialog extends React.Component {
const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
buttons = ;
}
return (
diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx
index 9613b27d17..a848bf2773 100644
--- a/src/components/views/dialogs/UserSettingsDialog.tsx
+++ b/src/components/views/dialogs/UserSettingsDialog.tsx
@@ -33,6 +33,7 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
+import { IDialogProps } from "./IDialogProps";
export enum UserTab {
General = "USER_GENERAL_TAB",
@@ -47,8 +48,7 @@ export enum UserTab {
Help = "USER_HELP_TAB",
}
-interface IProps {
- onFinished: (success: boolean) => void;
+interface IProps extends IDialogProps {
initialTabId?: string;
}
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
index 7993f9c418..81861c7f4d 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
@@ -25,6 +25,8 @@ import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
+import { logger } from "matrix-js-sdk/src/logger";
+
interface IProps extends IDialogProps {
widget: Widget;
widgetKind: WidgetKind;
@@ -45,17 +47,17 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent {
+ private onAllow = (): void => {
this.onPermissionSelection(true);
};
- private onDeny = () => {
+ private onDeny = (): void => {
this.onPermissionSelection(false);
};
- private onPermissionSelection(allowed: boolean) {
+ private onPermissionSelection(allowed: boolean): void {
if (this.state.rememberSelection) {
- console.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
+ logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
WidgetPermissionStore.instance.setOIDCState(
this.props.widget, this.props.widgetKind, this.props.inRoomId,
@@ -66,11 +68,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent {
+ private onRememberSelectionChange = (newVal: boolean): void => {
this.setState({ rememberSelection: newVal });
};
- public render() {
+ public render(): JSX.Element {
return (
{
diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
similarity index 75%
rename from src/components/views/dialogs/security/RestoreKeyBackupDialog.js
rename to src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
index 2b272a3b88..b651da5121 100644
--- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js
+++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
@@ -16,30 +16,64 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';
+import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
+import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
+import * as sdk from '../../../../index';
+import { IDialogProps } from "../IDialogProps";
+import { logger } from "matrix-js-sdk/src/logger";
-const RESTORE_TYPE_PASSPHRASE = 0;
-const RESTORE_TYPE_RECOVERYKEY = 1;
-const RESTORE_TYPE_SECRET_STORAGE = 2;
+enum RestoreType {
+ Passphrase = "passphrase",
+ RecoveryKey = "recovery_key",
+ SecretStorage = "secret_storage"
+}
+
+enum ProgressState {
+ PreFetch = "prefetch",
+ Fetch = "fetch",
+ LoadKeys = "load_keys",
+
+}
+
+interface IProps extends IDialogProps {
+ // if false, will close the dialog as soon as the restore completes succesfully
+ // default: true
+ showSummary?: boolean;
+ // If specified, gather the key from the user but then call the function with the backup
+ // key rather than actually (necessarily) restoring the backup.
+ keyCallback?: (key: Uint8Array) => void;
+}
+
+interface IState {
+ backupInfo: IKeyBackupInfo;
+ backupKeyStored: Record;
+ loading: boolean;
+ loadError: string;
+ restoreError: {
+ errcode: string;
+ };
+ recoveryKey: string;
+ recoverInfo: IKeyBackupRestoreResult;
+ recoveryKeyValid: boolean;
+ forceRecoveryKey: boolean;
+ passPhrase: string;
+ restoreType: RestoreType;
+ progress: {
+ stage: ProgressState;
+ total?: number;
+ successes?: number;
+ failures?: number;
+ };
+}
/*
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
-export default class RestoreKeyBackupDialog extends React.PureComponent {
- static propTypes = {
- // if false, will close the dialog as soon as the restore completes succesfully
- // default: true
- showSummary: PropTypes.bool,
- // If specified, gather the key from the user but then call the function with the backup
- // key rather than actually (necessarily) restoring the backup.
- keyCallback: PropTypes.func,
- };
-
+export default class RestoreKeyBackupDialog extends React.PureComponent {
static defaultProps = {
showSummary: true,
};
@@ -58,58 +92,58 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
forceRecoveryKey: false,
passPhrase: '',
restoreType: null,
- progress: { stage: "prefetch" },
+ progress: { stage: ProgressState.PreFetch },
};
}
- componentDidMount() {
- this._loadBackupStatus();
+ public componentDidMount(): void {
+ this.loadBackupStatus();
}
- _onCancel = () => {
+ private onCancel = (): void => {
this.props.onFinished(false);
- }
+ };
- _onDone = () => {
+ private onDone = (): void => {
this.props.onFinished(true);
- }
+ };
- _onUseRecoveryKeyClick = () => {
+ private onUseRecoveryKeyClick = (): void => {
this.setState({
forceRecoveryKey: true,
});
- }
+ };
- _progressCallback = (data) => {
+ private progressCallback = (data): void => {
this.setState({
progress: data,
});
- }
+ };
- _onResetRecoveryClick = () => {
+ private onResetRecoveryClick = (): void => {
this.props.onFinished(false);
- accessSecretStorage(() => {}, /* forceReset = */ true);
- }
+ accessSecretStorage(async () => {}, /* forceReset = */ true);
+ };
- _onRecoveryKeyChange = (e) => {
+ private onRecoveryKeyChange = (e): void => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
});
- }
+ };
- _onPassPhraseNext = async () => {
+ private onPassPhraseNext = async (): Promise => {
this.setState({
loading: true,
restoreError: null,
- restoreType: RESTORE_TYPE_PASSPHRASE,
+ restoreType: RestoreType.Passphrase,
});
try {
// We do still restore the key backup: we must ensure that the key backup key
// is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
- { progressCallback: this._progressCallback },
+ { progressCallback: this.progressCallback },
);
if (this.props.keyCallback) {
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
@@ -127,26 +161,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo,
});
} catch (e) {
- console.log("Error restoring backup", e);
+ logger.log("Error restoring backup", e);
this.setState({
loading: false,
restoreError: e,
});
}
- }
+ };
- _onRecoveryKeyNext = async () => {
+ private onRecoveryKeyNext = async (): Promise => {
if (!this.state.recoveryKeyValid) return;
this.setState({
loading: true,
restoreError: null,
- restoreType: RESTORE_TYPE_RECOVERYKEY,
+ restoreType: RestoreType.RecoveryKey,
});
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
- { progressCallback: this._progressCallback },
+ { progressCallback: this.progressCallback },
);
if (this.props.keyCallback) {
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
@@ -161,40 +195,39 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo,
});
} catch (e) {
- console.log("Error restoring backup", e);
+ logger.log("Error restoring backup", e);
this.setState({
loading: false,
restoreError: e,
});
}
- }
+ };
- _onPassPhraseChange = (e) => {
+ private onPassPhraseChange = (e): void => {
this.setState({
passPhrase: e.target.value,
});
- }
+ };
- async _restoreWithSecretStorage() {
+ private async restoreWithSecretStorage(): Promise {
this.setState({
loading: true,
restoreError: null,
- restoreType: RESTORE_TYPE_SECRET_STORAGE,
+ restoreType: RestoreType.SecretStorage,
});
try {
// `accessSecretStorage` may prompt for storage access as needed.
- const recoverInfo = await accessSecretStorage(async () => {
- return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
+ await accessSecretStorage(async () => {
+ await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo, undefined, undefined,
- { progressCallback: this._progressCallback },
+ { progressCallback: this.progressCallback },
);
});
this.setState({
loading: false,
- recoverInfo,
});
} catch (e) {
- console.log("Error restoring backup", e);
+ logger.log("Error restoring backup", e);
this.setState({
restoreError: e,
loading: false,
@@ -202,26 +235,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}
}
- async _restoreWithCachedKey(backupInfo) {
+ private async restoreWithCachedKey(backupInfo): Promise {
if (!backupInfo) return false;
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
undefined, /* targetRoomId */
undefined, /* targetSessionId */
backupInfo,
- { progressCallback: this._progressCallback },
+ { progressCallback: this.progressCallback },
);
this.setState({
recoverInfo,
});
return true;
} catch (e) {
- console.log("restoreWithCachedKey failed:", e);
+ logger.log("restoreWithCachedKey failed:", e);
return false;
}
}
- async _loadBackupStatus() {
+ private async loadBackupStatus(): Promise {
this.setState({
loading: true,
loadError: null,
@@ -230,15 +263,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey();
- const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
+ const backupKeyStored = has4S && (await cli.isKeyBackupKeyStored());
this.setState({
backupInfo,
backupKeyStored,
});
- const gotCache = await this._restoreWithCachedKey(backupInfo);
+ const gotCache = await this.restoreWithCachedKey(backupInfo);
if (gotCache) {
- console.log("RestoreKeyBackupDialog: found cached backup key");
+ logger.log("RestoreKeyBackupDialog: found cached backup key");
this.setState({
loading: false,
});
@@ -247,7 +280,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// If the backup key is stored, we can proceed directly to restore.
if (backupKeyStored) {
- return this._restoreWithSecretStorage();
+ return this.restoreWithSecretStorage();
}
this.setState({
@@ -255,7 +288,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loading: false,
});
} catch (e) {
- console.log("Error loading backup status", e);
+ logger.log("Error loading backup status", e);
this.setState({
loadError: e,
loading: false,
@@ -263,7 +296,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}
}
- render() {
+ public render(): JSX.Element {
+ // FIXME: Making these into imports will break tests
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner");
@@ -279,12 +315,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
if (this.state.loading) {
title = _t("Restoring keys from backup");
let details;
- if (this.state.progress.stage === "fetch") {
+ if (this.state.progress.stage === ProgressState.Fetch) {
details = _t("Fetching keys from server...");
- } else if (this.state.progress.stage === "load_keys") {
+ } else if (this.state.progress.stage === ProgressState.LoadKeys) {
const { total, successes, failures } = this.state.progress;
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
- } else if (this.state.progress.stage === "prefetch") {
+ } else if (this.state.progress.stage === ProgressState.PreFetch) {
details = _t("Fetching keys from server...");
}
content =
@@ -296,7 +332,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = _t("Unable to load backup status");
} else if (this.state.restoreError) {
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
- if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
+ if (this.state.restoreType === RestoreType.RecoveryKey) {
title = _t("Security Key mismatch");
content =
{ _t(
@@ -321,7 +357,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Error");
content = _t("No backup found!");
} else if (this.state.recoverInfo) {
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Keys restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
@@ -334,14 +369,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
);
@@ -390,8 +454,8 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement }
@@ -407,7 +471,7 @@ export default class AppTile extends React.Component {
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody =
}
@@ -469,49 +533,3 @@ export default class AppTile extends React.Component {
;
}
}
-
-AppTile.displayName = 'AppTile';
-
-AppTile.propTypes = {
- app: PropTypes.object.isRequired,
- // If room is not specified then it is an account level widget
- // which bypasses permission prompts as it was added explicitly by that user
- room: PropTypes.object,
- // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
- // This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
- fullWidth: PropTypes.bool,
- // Optional. If set, renders a smaller view of the widget
- miniMode: PropTypes.bool,
- // UserId of the current user
- userId: PropTypes.string.isRequired,
- // UserId of the entity that added / modified the widget
- creatorUserId: PropTypes.string,
- waitForIframeLoad: PropTypes.bool,
- showMenubar: PropTypes.bool,
- // Optional onEditClickHandler (overrides default behaviour)
- onEditClick: PropTypes.func,
- // Optional onDeleteClickHandler (overrides default behaviour)
- onDeleteClick: PropTypes.func,
- // Optional onMinimiseClickHandler
- onMinimiseClick: PropTypes.func,
- // Optionally hide the tile title
- showTitle: PropTypes.bool,
- // Optionally handle minimise button pointer events (default false)
- handleMinimisePointerEvents: PropTypes.bool,
- // Optionally hide the popout widget icon
- showPopout: PropTypes.bool,
- // Is this an instance of a user widget
- userWidget: PropTypes.bool,
- // sets the pointer-events property on the iframe
- pointerEvents: PropTypes.string,
-};
-
-AppTile.defaultProps = {
- waitForIframeLoad: true,
- showMenubar: true,
- showTitle: true,
- showPopout: true,
- handleMinimisePointerEvents: false,
- userWidget: false,
- miniMode: false,
-};
diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.tsx
similarity index 60%
rename from src/components/views/elements/AppWarning.js
rename to src/components/views/elements/AppWarning.tsx
index 517242dab5..bac486d4b8 100644
--- a/src/components/views/elements/AppWarning.js
+++ b/src/components/views/elements/AppWarning.tsx
@@ -1,24 +1,20 @@
-import React from 'react'; // eslint-disable-line no-unused-vars
-import PropTypes from 'prop-types';
+import React from 'react';
-const AppWarning = (props) => {
+interface IProps {
+ errorMsg?: string;
+}
+
+const AppWarning: React.FC = (props) => {
return (
);
};
-AppWarning.propTypes = {
- errorMsg: PropTypes.string,
-};
-AppWarning.defaultProps = {
- errorMsg: 'Error',
-};
-
export default AppWarning;
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.tsx
similarity index 64%
rename from src/components/views/elements/DialogButtons.js
rename to src/components/views/elements/DialogButtons.tsx
index 9dd4a84b9a..0dff64c0b4 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.tsx
@@ -17,60 +17,61 @@ limitations under the License.
*/
import React from "react";
-import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+interface IProps {
+ // The primary button which is styled differently and has default focus.
+ primaryButton: React.ReactNode;
+
+ // A node to insert into the cancel button instead of default "Cancel"
+ cancelButton?: React.ReactNode;
+
+ // If true, make the primary button a form submit button (input type="submit")
+ primaryIsSubmit?: boolean;
+
+ // onClick handler for the primary button.
+ onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
+
+ // should there be a cancel button? default: true
+ hasCancel?: boolean;
+
+ // The class of the cancel button, only used if a cancel button is
+ // enabled
+ cancelButtonClass?: string;
+
+ // onClick handler for the cancel button.
+ onCancel?: (...args: any[]) => void;
+
+ focus?: boolean;
+
+ // disables the primary and cancel buttons
+ disabled?: boolean;
+
+ // disables only the primary button
+ primaryDisabled?: boolean;
+
+ // something to stick next to the buttons, optionally
+ additive?: React.ReactNode;
+
+ primaryButtonClass?: string;
+}
+
/**
* Basic container for buttons in modal dialogs.
*/
@replaceableComponent("views.elements.DialogButtons")
-export default class DialogButtons extends React.Component {
- static propTypes = {
- // The primary button which is styled differently and has default focus.
- primaryButton: PropTypes.node.isRequired,
-
- // A node to insert into the cancel button instead of default "Cancel"
- cancelButton: PropTypes.node,
-
- // If true, make the primary button a form submit button (input type="submit")
- primaryIsSubmit: PropTypes.bool,
-
- // onClick handler for the primary button.
- onPrimaryButtonClick: PropTypes.func,
-
- // should there be a cancel button? default: true
- hasCancel: PropTypes.bool,
-
- // The class of the cancel button, only used if a cancel button is
- // enabled
- cancelButtonClass: PropTypes.node,
-
- // onClick handler for the cancel button.
- onCancel: PropTypes.func,
-
- focus: PropTypes.bool,
-
- // disables the primary and cancel buttons
- disabled: PropTypes.bool,
-
- // disables only the primary button
- primaryDisabled: PropTypes.bool,
-
- // something to stick next to the buttons, optionally
- additive: PropTypes.element,
- };
-
- static defaultProps = {
+export default class DialogButtons extends React.Component {
+ public static defaultProps: Partial = {
hasCancel: true,
disabled: false,
};
- _onCancelClick = () => {
- this.props.onCancel();
+ private onCancelClick = (event: React.MouseEvent): void => {
+ this.props.onCancel(event);
};
- render() {
+ public render(): JSX.Element {
let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
@@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
// important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit.
type="button"
- onClick={this._onCancelClick}
+ onClick={this.onCancelClick}
className={this.props.cancelButtonClass}
disabled={this.props.disabled}
>
diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.tsx
similarity index 63%
rename from src/components/views/elements/DirectorySearchBox.js
rename to src/components/views/elements/DirectorySearchBox.tsx
index 11b1ed2cd2..07407af622 100644
--- a/src/components/views/elements/DirectorySearchBox.js
+++ b/src/components/views/elements/DirectorySearchBox.tsx
@@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
+import React, { ChangeEvent, createRef } from 'react';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import AccessibleButton from "./AccessibleButton";
+
+interface IProps {
+ className?: string;
+ onChange?: (value: string) => void;
+ onClear?: () => void;
+ onJoinClick?: (value: string) => void;
+ placeholder?: string;
+ showJoinButton?: boolean;
+ initialText?: string;
+}
+
+interface IState {
+ value: string;
+}
@replaceableComponent("views.elements.DirectorySearchBox")
-export default class DirectorySearchBox extends React.Component {
- constructor(props) {
- super(props);
- this._collectInput = this._collectInput.bind(this);
- this._onClearClick = this._onClearClick.bind(this);
- this._onChange = this._onChange.bind(this);
- this._onKeyUp = this._onKeyUp.bind(this);
- this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
+export default class DirectorySearchBox extends React.Component {
+ private input = createRef();
- this.input = null;
+ constructor(props: IProps) {
+ super(props);
this.state = {
value: this.props.initialText || '',
};
}
- _collectInput(e) {
- this.input = e;
- }
-
- _onClearClick() {
+ private onClearClick = (): void => {
this.setState({ value: '' });
- if (this.input) {
- this.input.focus();
+ if (this.input.current) {
+ this.input.current.focus();
if (this.props.onClear) {
this.props.onClear();
}
}
- }
+ };
- _onChange(ev) {
- if (!this.input) return;
+ private onChange = (ev: ChangeEvent): void => {
+ if (!this.input.current) return;
this.setState({ value: ev.target.value });
if (this.props.onChange) {
this.props.onChange(ev.target.value);
}
- }
+ };
- _onKeyUp(ev) {
+ private onKeyUp = (ev: React.KeyboardEvent): void => {
if (ev.key == 'Enter' && this.props.showJoinButton) {
if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value);
}
}
- }
+ };
- _onJoinButtonClick() {
+ private onJoinButtonClick = (): void => {
if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value);
}
- }
-
- render() {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+ };
+ public render(): JSX.Element {
const searchboxClasses = {
mx_DirectorySearchBox: true,
};
@@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
let joinButton;
if (this.props.showJoinButton) {
joinButton = { _t("Join") };
}
@@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
name="dirsearch"
value={this.state.value}
className="mx_textinput_icon mx_textinput_search"
- ref={this._collectInput}
- onChange={this._onChange}
- onKeyUp={this._onKeyUp}
+ ref={this.input}
+ onChange={this.onChange}
+ onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder}
autoFocus
/>
{ joinButton }
-
+
;
}
}
-DirectorySearchBox.propTypes = {
- className: PropTypes.string,
- onChange: PropTypes.func,
- onClear: PropTypes.func,
- onJoinClick: PropTypes.func,
- placeholder: PropTypes.string,
- showJoinButton: PropTypes.bool,
- initialText: PropTypes.string,
-};
diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.tsx
similarity index 62%
rename from src/components/views/elements/EditableText.js
rename to src/components/views/elements/EditableText.tsx
index 6dbc8b8771..b3ff8ee245 100644
--- a/src/components/views/elements/EditableText.js
+++ b/src/components/views/elements/EditableText.tsx
@@ -16,33 +16,42 @@ limitations under the License.
*/
import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+enum Phases {
+ Display = "display",
+ Edit = "edit",
+}
+
+interface IProps {
+ onValueChanged?: (value: string, shouldSubmit: boolean) => void;
+ initialValue?: string;
+ label?: string;
+ placeholder?: string;
+ className?: string;
+ labelClassName?: string;
+ placeholderClassName?: string;
+ // Overrides blurToSubmit if true
+ blurToCancel?: boolean;
+ // Will cause onValueChanged(value, true) to fire on blur
+ blurToSubmit?: boolean;
+ editable?: boolean;
+}
+
+interface IState {
+ phase: Phases;
+}
+
@replaceableComponent("views.elements.EditableText")
-export default class EditableText extends React.Component {
- static propTypes = {
- onValueChanged: PropTypes.func,
- initialValue: PropTypes.string,
- label: PropTypes.string,
- placeholder: PropTypes.string,
- className: PropTypes.string,
- labelClassName: PropTypes.string,
- placeholderClassName: PropTypes.string,
- // Overrides blurToSubmit if true
- blurToCancel: PropTypes.bool,
- // Will cause onValueChanged(value, true) to fire on blur
- blurToSubmit: PropTypes.bool,
- editable: PropTypes.bool,
- };
+export default class EditableText extends React.Component {
+ // we track value as an JS object field rather than in React state
+ // as React doesn't play nice with contentEditable.
+ public value = '';
+ private placeholder = false;
+ private editableDiv = createRef();
- static Phases = {
- Display: "display",
- Edit: "edit",
- };
-
- static defaultProps = {
+ public static defaultProps: Partial = {
onValueChanged() {},
initialValue: '',
label: '',
@@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
blurToSubmit: false,
};
- constructor(props) {
+ constructor(props: IProps) {
super(props);
- // we track value as an JS object field rather than in React state
- // as React doesn't play nice with contentEditable.
- this.value = '';
- this.placeholder = false;
-
- this._editable_div = createRef();
+ this.state = {
+ phase: Phases.Display,
+ };
}
- state = {
- phase: EditableText.Phases.Display,
- };
-
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
+ // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
+ public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
- if (this._editable_div.current) {
+ if (this.editableDiv.current) {
this.showPlaceholder(!this.value);
}
}
}
- componentDidMount() {
+ public componentDidMount(): void {
this.value = this.props.initialValue;
- if (this._editable_div.current) {
+ if (this.editableDiv.current) {
this.showPlaceholder(!this.value);
}
}
- showPlaceholder = show => {
+ private showPlaceholder = (show: boolean): void => {
if (show) {
- this._editable_div.current.textContent = this.props.placeholder;
- this._editable_div.current.setAttribute("class", this.props.className
+ this.editableDiv.current.textContent = this.props.placeholder;
+ this.editableDiv.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName);
this.placeholder = true;
this.value = '';
} else {
- this._editable_div.current.textContent = this.value;
- this._editable_div.current.setAttribute("class", this.props.className);
+ this.editableDiv.current.textContent = this.value;
+ this.editableDiv.current.setAttribute("class", this.props.className);
this.placeholder = false;
}
};
- getValue = () => this.value;
-
- setValue = value => {
- this.value = value;
- this.showPlaceholder(!this.value);
- };
-
- edit = () => {
+ private cancelEdit = (): void => {
this.setState({
- phase: EditableText.Phases.Edit,
- });
- };
-
- cancelEdit = () => {
- this.setState({
- phase: EditableText.Phases.Display,
+ phase: Phases.Display,
});
this.value = this.props.initialValue;
this.showPlaceholder(!this.value);
this.onValueChanged(false);
- this._editable_div.current.blur();
+ this.editableDiv.current.blur();
};
- onValueChanged = shouldSubmit => {
+ private onValueChanged = (shouldSubmit: boolean): void => {
this.props.onValueChanged(this.value, shouldSubmit);
};
- onKeyDown = ev => {
+ private onKeyDown = (ev: React.KeyboardEvent): void => {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) {
@@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
};
- onKeyUp = ev => {
+ private onKeyUp = (ev: React.KeyboardEvent): void => {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
- if (!ev.target.textContent) {
+ if (!(ev.target as HTMLDivElement).textContent) {
this.showPlaceholder(true);
} else if (!this.placeholder) {
- this.value = ev.target.textContent;
+ this.value = (ev.target as HTMLDivElement).textContent;
}
if (ev.key === Key.ENTER) {
@@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
};
- onClickDiv = ev => {
+ private onClickDiv = (): void => {
if (!this.props.editable) return;
this.setState({
- phase: EditableText.Phases.Edit,
+ phase: Phases.Edit,
});
};
- onFocus = ev => {
+ private onFocus = (ev: React.FocusEvent): void => {
//ev.target.setSelectionRange(0, ev.target.textContent.length);
const node = ev.target.childNodes[0];
if (node) {
const range = document.createRange();
range.setStart(node, 0);
- range.setEnd(node, node.length);
+ range.setEnd(node, ev.target.childNodes.length);
const sel = window.getSelection();
sel.removeAllRanges();
@@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
}
};
- onFinish = (ev, shouldSubmit) => {
+ private onFinish = (
+ ev: React.KeyboardEvent | React.FocusEvent,
+ shouldSubmit?: boolean,
+ ): void => {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
- const submit = (ev.key === Key.ENTER) || shouldSubmit;
+ const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
this.setState({
- phase: EditableText.Phases.Display,
+ phase: Phases.Display,
}, () => {
if (this.value !== this.props.initialValue) {
self.onValueChanged(submit);
@@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
});
};
- onBlur = ev => {
+ private onBlur = (ev: React.FocusEvent): void => {
const sel = window.getSelection();
sel.removeAllRanges();
@@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
this.showPlaceholder(!this.value);
};
- render() {
+ public render(): JSX.Element {
const { className, editable, initialValue, label, labelClassName } = this.props;
let editableEl;
- if (!editable || (this.state.phase === EditableText.Phases.Display &&
+ if (!editable || (this.state.phase === Phases.Display &&
(label || labelClassName) && !this.value)
) {
// show the label
@@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
} else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl =
Promise;
+
+ /* initial value; used if getInitialValue is not given */
+ initialValue?: string;
+
+ /* placeholder text to use when the value is empty (and not being
+ * edited) */
+ placeholder?: string;
+
+ /* callback to update the value. Called with a single argument: the new
+ * value. */
+ onSubmit?: (value: string) => Promise<{} | void>;
+
+ /* should the input submit when focus is lost? */
+ blurToSubmit?: boolean;
+}
+
+interface IState {
+ busy: boolean;
+ errorString: string;
+ value: string;
+}
/**
* A component which wraps an EditableText, with a spinner while updates take
@@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* taken from the 'initialValue' property.
*/
@replaceableComponent("views.elements.EditableTextContainer")
-export default class EditableTextContainer extends React.Component {
- constructor(props) {
+export default class EditableTextContainer extends React.Component {
+ private unmounted = false;
+ public static defaultProps: Partial = {
+ initialValue: "",
+ placeholder: "",
+ blurToSubmit: false,
+ onSubmit: () => { return Promise.resolve(); },
+ };
+
+ constructor(props: IProps) {
super(props);
- this._unmounted = false;
this.state = {
busy: false,
errorString: null,
value: props.initialValue,
};
- this._onValueChanged = this._onValueChanged.bind(this);
}
- componentDidMount() {
- if (this.props.getInitialValue === undefined) {
- // use whatever was given in the initialValue property.
- return;
- }
+ public async componentDidMount(): Promise {
+ // use whatever was given in the initialValue property.
+ if (this.props.getInitialValue === undefined) return;
this.setState({ busy: true });
-
- this.props.getInitialValue().then(
- (result) => {
- if (this._unmounted) { return; }
- this.setState({
- busy: false,
- value: result,
- });
- },
- (error) => {
- if (this._unmounted) { return; }
- this.setState({
- errorString: error.toString(),
- busy: false,
- });
- },
- );
+ try {
+ const initialValue = await this.props.getInitialValue();
+ if (this.unmounted) return;
+ this.setState({
+ busy: false,
+ value: initialValue,
+ });
+ } catch (error) {
+ if (this.unmounted) return;
+ this.setState({
+ errorString: error.toString(),
+ busy: false,
+ });
+ }
}
- componentWillUnmount() {
- this._unmounted = true;
+ public componentWillUnmount(): void {
+ this.unmounted = true;
}
- _onValueChanged(value, shouldSubmit) {
+ private onValueChanged = (value: string, shouldSubmit: boolean): void => {
if (!shouldSubmit) {
return;
}
@@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
this.props.onSubmit(value).then(
() => {
- if (this._unmounted) { return; }
+ if (this.unmounted) { return; }
this.setState({
busy: false,
value: value,
});
},
(error) => {
- if (this._unmounted) { return; }
+ if (this.unmounted) { return; }
this.setState({
errorString: error.toString(),
busy: false,
});
},
);
- }
+ };
- render() {
+ public render(): JSX.Element {
if (this.state.busy) {
- const Loader = sdk.getComponent("elements.Spinner");
return (
-
+
);
} else if (this.state.errorString) {
return (
{ this.state.errorString }
);
} else {
- const EditableText = sdk.getComponent('elements.EditableText');
return (
);
@@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
}
}
-EditableTextContainer.propTypes = {
- /* callback to retrieve the initial value. */
- getInitialValue: PropTypes.func,
-
- /* initial value; used if getInitialValue is not given */
- initialValue: PropTypes.string,
-
- /* placeholder text to use when the value is empty (and not being
- * edited) */
- placeholder: PropTypes.string,
-
- /* callback to update the value. Called with a single argument: the new
- * value. */
- onSubmit: PropTypes.func,
-
- /* should the input submit when focus is lost? */
- blurToSubmit: PropTypes.bool,
-};
-
-EditableTextContainer.defaultProps = {
- initialValue: "",
- placeholder: "",
- blurToSubmit: false,
- onSubmit: function(v) {return Promise.resolve(); },
-};
diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx
index 7a1efb7a62..44ff6644d7 100644
--- a/src/components/views/elements/ImageView.tsx
+++ b/src/components/views/elements/ImageView.tsx
@@ -34,6 +34,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps';
+import UIStore from '../../../stores/UIStore';
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
@@ -44,6 +45,13 @@ const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10;
+// Height of mx_ImageView_panel
+const getPanelHeight = (): number => {
+ const value = getComputedStyle(document.documentElement).getPropertyValue("--image-view-panel-height");
+ // Return the value as a number without the unit
+ return parseInt(value.slice(0, value.length - 2));
+};
+
interface IProps extends IDialogProps {
src: string; // the source of the image being displayed
name?: string; // the main title ('name') for the image
@@ -56,8 +64,15 @@ interface IProps extends IDialogProps {
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated
// with events.
- mxEvent: MatrixEvent;
- permalinkCreator: RoomPermalinkCreator;
+ mxEvent?: MatrixEvent;
+ permalinkCreator?: RoomPermalinkCreator;
+
+ thumbnailInfo?: {
+ positionX: number;
+ positionY: number;
+ width: number;
+ height: number;
+ };
}
interface IState {
@@ -75,13 +90,25 @@ interface IState {
export default class ImageView extends React.Component {
constructor(props) {
super(props);
+
+ const { thumbnailInfo } = this.props;
+
this.state = {
- zoom: 0,
+ zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
minZoom: MAX_SCALE,
maxZoom: MAX_SCALE,
rotation: 0,
- translationX: 0,
- translationY: 0,
+ translationX: (
+ thumbnailInfo?.positionX +
+ (thumbnailInfo?.width / 2) -
+ (UIStore.instance.windowWidth / 2)
+ ) ?? 0,
+ translationY: (
+ thumbnailInfo?.positionY +
+ (thumbnailInfo?.height / 2) -
+ (UIStore.instance.windowHeight / 2) -
+ (getPanelHeight() / 2)
+ ) ?? 0,
moving: false,
contextMenuDisplayed: false,
};
@@ -98,6 +125,9 @@ export default class ImageView extends React.Component {
private previousX = 0;
private previousY = 0;
+ private animatingLoading = false;
+ private imageIsLoaded = false;
+
componentDidMount() {
// We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium
@@ -105,15 +135,37 @@ export default class ImageView extends React.Component {
// We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom
- this.image.current.addEventListener("load", this.recalculateZoom);
+ this.image.current.addEventListener("load", this.imageLoaded);
}
componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.recalculateZoom);
- this.image.current.removeEventListener("load", this.recalculateZoom);
+ this.image.current.removeEventListener("load", this.imageLoaded);
}
+ private imageLoaded = () => {
+ // First, we calculate the zoom, so that the image has the same size as
+ // the thumbnail
+ const { thumbnailInfo } = this.props;
+ if (thumbnailInfo?.width) {
+ this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth });
+ }
+
+ // Once the zoom is set, we the image is considered loaded and we can
+ // start animating it into the center of the screen
+ this.imageIsLoaded = true;
+ this.animatingLoading = true;
+ this.setZoomAndRotation();
+ this.setState({
+ translationX: 0,
+ translationY: 0,
+ });
+
+ // Once the position is set, there is no need to animate anymore
+ this.animatingLoading = false;
+ };
+
private recalculateZoom = () => {
this.setZoomAndRotation();
};
@@ -360,16 +412,17 @@ export default class ImageView extends React.Component {
const showEventMeta = !!this.props.mxEvent;
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
+ let transitionClassName;
+ if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading";
+ else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
+ else transitionClassName = "mx_ImageView_image_animating";
+
let cursor;
- if (this.state.moving) {
- cursor= "grabbing";
- } else if (zoomingDisabled) {
- cursor = "default";
- } else if (this.state.zoom === this.state.minZoom) {
- cursor = "zoom-in";
- } else {
- cursor = "zoom-out";
- }
+ if (this.state.moving) cursor = "grabbing";
+ else if (zoomingDisabled) cursor = "default";
+ else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in";
+ else cursor = "zoom-out";
+
const rotationDegrees = this.state.rotation + "deg";
const zoom = this.state.zoom;
const translatePixelsX = this.state.translationX + "px";
@@ -380,7 +433,6 @@ export default class ImageView extends React.Component {
// image causing it translate in the wrong direction.
const style = {
cursor: cursor,
- transition: this.state.moving ? null : "transform 200ms ease 0s",
transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY})
scale(${zoom})
@@ -528,7 +580,7 @@ export default class ImageView extends React.Component {
style={style}
alt={this.props.name}
ref={this.image}
- className="mx_ImageView_image"
+ className={`mx_ImageView_image ${transitionClassName}`}
draggable={true}
onMouseDown={this.onStartMoving}
/>
diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.tsx
similarity index 84%
rename from src/components/views/elements/LanguageDropdown.js
rename to src/components/views/elements/LanguageDropdown.tsx
index 3f17a78629..c6c52ee4e8 100644
--- a/src/components/views/elements/LanguageDropdown.js
+++ b/src/components/views/elements/LanguageDropdown.tsx
@@ -16,13 +16,13 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Spinner from "./Spinner";
+import Dropdown from "./Dropdown";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
return false;
}
+interface IProps {
+ className?: string;
+ onOptionChange: (language: string) => void;
+ value?: string;
+ disabled?: boolean;
+}
+
+interface IState {
+ searchQuery: string;
+ langs: string[];
+}
+
@replaceableComponent("views.elements.LanguageDropdown")
-export default class LanguageDropdown extends React.Component {
- constructor(props) {
+export default class LanguageDropdown extends React.Component {
+ constructor(props: IProps) {
super(props);
- this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
@@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
};
}
- componentDidMount() {
+ public componentDidMount(): void {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) {
if (a.label < b.label) return -1;
@@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
}
}
- _onSearchChange(search) {
+ private onSearchChange = (search: string): void => {
this.setState({
searchQuery: search,
});
- }
+ };
- render() {
+ public render(): JSX.Element {
if (this.state.langs === null) {
- const Spinner = sdk.getComponent('elements.Spinner');
return ;
}
- const Dropdown = sdk.getComponent('elements.Dropdown');
-
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => {
@@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
id="mx_LanguageDropdown"
className={this.props.className}
onOptionChange={this.props.onOptionChange}
- onSearchChange={this._onSearchChange}
+ onSearchChange={this.onSearchChange}
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
@@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
}
}
-LanguageDropdown.propTypes = {
- className: PropTypes.string,
- onOptionChange: PropTypes.func.isRequired,
- value: PropTypes.string,
-};
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.tsx
similarity index 80%
rename from src/components/views/elements/LazyRenderList.js
rename to src/components/views/elements/LazyRenderList.tsx
index 070d9bcc8d..54c76f27a7 100644
--- a/src/components/views/elements/LazyRenderList.js
+++ b/src/components/views/elements/LazyRenderList.tsx
@@ -15,17 +15,16 @@ limitations under the License.
*/
import React from "react";
-import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
class ItemRange {
- constructor(topCount, renderCount, bottomCount) {
- this.topCount = topCount;
- this.renderCount = renderCount;
- this.bottomCount = bottomCount;
- }
+ constructor(
+ public topCount: number,
+ public renderCount: number,
+ public bottomCount: number,
+ ) { }
- contains(range) {
+ public contains(range: ItemRange): boolean {
// don't contain empty ranges
// as it will prevent clearing the list
// once it is scrolled far enough out of view
@@ -36,7 +35,7 @@ class ItemRange {
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
}
- expand(amount) {
+ public expand(amount: number): ItemRange {
// don't expand ranges that won't render anything
if (this.renderCount === 0) {
return this;
@@ -51,20 +50,55 @@ class ItemRange {
);
}
- totalSize() {
+ public totalSize(): number {
return this.topCount + this.renderCount + this.bottomCount;
}
}
+interface IProps {
+ // height in pixels of the component returned by `renderItem`
+ itemHeight: number;
+ // function to turn an element of `items` into a react component
+ renderItem: (item: T) => JSX.Element;
+ // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
+ scrollTop: number;
+ // the height of the viewport this content is scrolled in
+ height: number;
+ // all items for the list. These should not be react components, see `renderItem`.
+ items?: T[];
+ // the amount of items to scroll before causing a rerender,
+ // should typically be less than `overflowItems` unless applying
+ // margins in the parent component when using multiple LazyRenderList in one viewport.
+ // use 0 to only rerender when items will come into view.
+ overflowMargin?: number;
+ // the amount of items to add at the top and bottom to render,
+ // so not every scroll of causes a rerender.
+ overflowItems?: number;
+
+ element?: string;
+ className?: string;
+}
+
+interface IState {
+ renderRange: ItemRange;
+}
+
@replaceableComponent("views.elements.LazyRenderList")
-export default class LazyRenderList extends React.Component {
- constructor(props) {
+export default class LazyRenderList extends React.Component, IState> {
+ public static defaultProps: Partial> = {
+ overflowItems: 20,
+ overflowMargin: 5,
+ };
+
+ constructor(props: IProps) {
super(props);
- this.state = {};
+ this.state = {
+ renderRange: null,
+ };
}
- static getDerivedStateFromProps(props, state) {
+ public static getDerivedStateFromProps(props: IProps, state: IState): Partial {
const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(props.overflowMargin);
const renderRange = range.expand(props.overflowItems);
@@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
return null;
}
- static getVisibleRangeFromProps(props) {
+ private static getVisibleRangeFromProps(props: IProps): ItemRange {
const { items, itemHeight, scrollTop, height } = props;
const length = items ? items.length : 0;
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
@@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
return new ItemRange(topCount, renderCount, bottomCount);
}
- render() {
+ public render(): JSX.Element {
const { itemHeight, items, renderItem } = this.props;
const { renderRange } = this.state;
const { topCount, renderCount, bottomCount } = renderRange;
@@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
}
}
-LazyRenderList.defaultProps = {
- overflowItems: 20,
- overflowMargin: 5,
-};
-
-LazyRenderList.propTypes = {
- // height in pixels of the component returned by `renderItem`
- itemHeight: PropTypes.number.isRequired,
- // function to turn an element of `items` into a react component
- renderItem: PropTypes.func.isRequired,
- // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
- scrollTop: PropTypes.number.isRequired,
- // the height of the viewport this content is scrolled in
- height: PropTypes.number.isRequired,
- // all items for the list. These should not be react components, see `renderItem`.
- items: PropTypes.array,
- // the amount of items to scroll before causing a rerender,
- // should typically be less than `overflowItems` unless applying
- // margins in the parent component when using multiple LazyRenderList in one viewport.
- // use 0 to only rerender when items will come into view.
- overflowMargin: PropTypes.number,
- // the amount of items to add at the top and bottom to render,
- // so not every scroll of causes a rerender.
- overflowItems: PropTypes.number,
-};
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index 0722cb872a..4eb0177fef 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component {
const desc = formatCommaSeparatedList(descs);
- return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
+ return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
});
if (!summaries) {
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.tsx
similarity index 69%
rename from src/components/views/elements/PersistedElement.js
rename to src/components/views/elements/PersistedElement.tsx
index 03aa9e0d6d..d013091803 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.tsx
@@ -16,25 +16,26 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
import { throttle } from "lodash";
-import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { ActionPayload } from "../../../dispatcher/payloads";
+
+export const getPersistKey = (appId: string) => 'widget_' + appId;
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
-function getContainer(containerId) {
- return document.getElementById(containerId);
+function getContainer(containerId: string): HTMLDivElement {
+ return document.getElementById(containerId) as HTMLDivElement;
}
-function getOrCreateContainer(containerId) {
+function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId);
if (!container) {
@@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
return container;
}
-/*
+interface IProps {
+ // Unique identifier for this PersistedElement instance
+ // Any PersistedElements with the same persistKey will use
+ // the same DOM container.
+ persistKey: string;
+
+ // z-index for the element. Defaults to 9.
+ zIndex?: number;
+
+ style?: React.StyleHTMLAttributes;
+}
+
+/**
* Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body.
*
@@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE.
*/
@replaceableComponent("views.elements.PersistedElement")
-export default class PersistedElement extends React.Component {
- static propTypes = {
- // Unique identifier for this PersistedElement instance
- // Any PersistedElements with the same persistKey will use
- // the same DOM container.
- persistKey: PropTypes.string.isRequired,
+export default class PersistedElement extends React.Component {
+ private resizeObserver: ResizeObserver;
+ private dispatcherRef: string;
+ private childContainer: HTMLDivElement;
+ private child: HTMLDivElement;
- // z-index for the element. Defaults to 9.
- zIndex: PropTypes.number,
- };
+ constructor(props: IProps) {
+ super(props);
- constructor() {
- super();
- this.collectChildContainer = this.collectChildContainer.bind(this);
- this.collectChild = this.collectChild.bind(this);
- this._repositionChild = this._repositionChild.bind(this);
- this._onAction = this._onAction.bind(this);
-
- this.resizeObserver = new ResizeObserver(this._repositionChild);
+ this.resizeObserver = new ResizeObserver(this.repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and
// the timeline_resize action.
- window.addEventListener('resize', this._repositionChild);
- this._dispatcherRef = dis.register(this._onAction);
+ window.addEventListener('resize', this.repositionChild);
+ this.dispatcherRef = dis.register(this.onAction);
}
/**
* Removes the DOM elements created when a PersistedElement with the given
* persistKey was mounted. The DOM elements will be re-added if another
- * PeristedElement is mounted in the future.
+ * PersistedElement is mounted in the future.
*
* @param {string} persistKey Key used to uniquely identify this PersistedElement
*/
- static destroyElement(persistKey) {
+ public static destroyElement(persistKey: string): void {
const container = getContainer('mx_persistedElement_' + persistKey);
if (container) {
container.remove();
@@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
return Boolean(getContainer('mx_persistedElement_' + persistKey));
}
- collectChildContainer(ref) {
+ private collectChildContainer = (ref: HTMLDivElement): void => {
if (this.childContainer) {
this.resizeObserver.unobserve(this.childContainer);
}
@@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
if (ref) {
this.resizeObserver.observe(ref);
}
- }
+ };
- collectChild(ref) {
+ private collectChild = (ref: HTMLDivElement): void => {
this.child = ref;
this.updateChild();
- }
+ };
- componentDidMount() {
+ public componentDidMount(): void {
this.updateChild();
this.renderApp();
}
- componentDidUpdate() {
+ public componentDidUpdate(): void {
this.updateChild();
this.renderApp();
}
- componentWillUnmount() {
+ public componentWillUnmount(): void {
this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect();
- window.removeEventListener('resize', this._repositionChild);
- dis.unregister(this._dispatcherRef);
+ window.removeEventListener('resize', this.repositionChild);
+ dis.unregister(this.dispatcherRef);
}
- _onAction(payload) {
+ private onAction = (payload: ActionPayload): void => {
if (payload.action === 'timeline_resize') {
- this._repositionChild();
+ this.repositionChild();
} else if (payload.action === 'logout') {
PersistedElement.destroyElement(this.props.persistKey);
}
- }
+ };
- _repositionChild() {
+ private repositionChild = (): void => {
this.updateChildPosition(this.child, this.childContainer);
- }
+ };
- updateChild() {
+ private updateChild(): void {
this.updateChildPosition(this.child, this.childContainer);
this.updateChildVisibility(this.child, true);
}
- renderApp() {
+ private renderApp(): void {
const content =
{ this.props.children }
@@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
}
- updateChildVisibility(child, visible) {
+ private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
if (!child) return;
child.style.display = visible ? 'block' : 'none';
}
- updateChildPosition = throttle((child, parent) => {
+ private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
@@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
});
}, 100, { trailing: true, leading: true });
- render() {
+ public render(): JSX.Element {
return ;
}
}
-export const getPersistKey = (appId) => 'widget_' + appId;
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.tsx
similarity index 60%
rename from src/components/views/elements/PersistentApp.js
rename to src/components/views/elements/PersistentApp.tsx
index 763ab63487..8d0751cc1d 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.tsx
@@ -17,61 +17,74 @@ limitations under the License.
import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore';
-import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
+import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils';
-import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { EventSubscription } from 'fbemitter';
+import AppTile from "./AppTile";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+interface IState {
+ roomId: string;
+ persistentWidgetId: string;
+}
@replaceableComponent("views.elements.PersistentApp")
-export default class PersistentApp extends React.Component {
- state = {
- roomId: RoomViewStore.getRoomId(),
- persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
- };
+export default class PersistentApp extends React.Component<{}, IState> {
+ private roomStoreToken: EventSubscription;
- componentDidMount() {
- this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
- ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
- MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
+ constructor() {
+ super({});
+
+ this.state = {
+ roomId: RoomViewStore.getRoomId(),
+ persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
+ };
}
- componentWillUnmount() {
- if (this._roomStoreToken) {
- this._roomStoreToken.remove();
+ public componentDidMount(): void {
+ this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
+ ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
+ MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
+ }
+
+ public componentWillUnmount(): void {
+ if (this.roomStoreToken) {
+ this.roomStoreToken.remove();
}
- ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
+ ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
+ MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
}
}
- _onRoomViewStoreUpdate = payload => {
+ private onRoomViewStoreUpdate = (): void => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({
roomId: RoomViewStore.getRoomId(),
});
};
- _onActiveWidgetStoreUpdate = () => {
+ private onActiveWidgetStoreUpdate = (): void => {
this.setState({
- persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
+ persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
});
};
- _onMyMembership = async (room, membership) => {
- const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
+ private onMyMembership = async (room: Room, membership: string): Promise => {
+ const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") {
// we're not in the room anymore - delete
- if (room.roomId === persistentWidgetInRoomId) {
- ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
+ if (room .roomId === persistentWidgetInRoomId) {
+ ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
}
}
};
- render() {
+ public render(): JSX.Element {
if (this.state.persistentWidgetId) {
- const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
+ const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
@@ -83,13 +96,12 @@ export default class PersistentApp extends React.Component {
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
- return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
+ return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
});
const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(),
);
- const AppTile = sdk.getComponent('elements.AppTile');
return void;
+
+ // Optional key to pass as the second argument to `onChange`
+ powerLevelKey?: string;
+
+ // The name to annotate the selector with
+ label?: string;
+}
+
+interface IState {
+ levelRoleMap: {};
+ // List of power levels to show in the drop-down
+ options: number[];
+
+ customValue: number;
+ selectValue: number | string;
+ custom?: boolean;
+ customLevel?: number;
+}
+
@replaceableComponent("views.elements.PowerSelector")
-export default class PowerSelector extends React.Component {
- static propTypes = {
- value: PropTypes.number.isRequired,
- // The maximum value that can be set with the power selector
- maxValue: PropTypes.number.isRequired,
-
- // Default user power level for the room
- usersDefault: PropTypes.number.isRequired,
-
- // should the user be able to change the value? false by default.
- disabled: PropTypes.bool,
- onChange: PropTypes.func,
-
- // Optional key to pass as the second argument to `onChange`
- powerLevelKey: PropTypes.string,
-
- // The name to annotate the selector with
- label: PropTypes.string,
- }
-
- static defaultProps = {
+export default class PowerSelector extends React.Component {
+ public static defaultProps: Partial = {
maxValue: Infinity,
usersDefault: 0,
};
- constructor(props) {
+ constructor(props: IProps) {
super(props);
this.state = {
@@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillMount() {
- this._initStateFromProps(this.props);
+ // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
+ public UNSAFE_componentWillMount(): void {
+ this.initStateFromProps(this.props);
}
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(newProps) {
- this._initStateFromProps(newProps);
+ // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
+ public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
+ this.initStateFromProps(newProps);
}
- _initStateFromProps(newProps) {
+ private initStateFromProps(newProps: IProps): void {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => {
return (
level === undefined ||
- level <= newProps.maxValue ||
- level == newProps.value
+ parseInt(level) <= newProps.maxValue ||
+ parseInt(level) == newProps.value
);
- });
+ }).map(level => parseInt(level));
const isCustom = levelRoleMap[newProps.value] === undefined;
@@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
options,
custom: isCustom,
customLevel: newProps.value,
- selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
+ selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
});
}
- onSelectChange = event => {
- const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
+ private onSelectChange = (event: React.ChangeEvent): void => {
+ const isCustom = event.target.value === CUSTOM_VALUE;
if (isCustom) {
this.setState({ custom: true });
} else {
- this.props.onChange(event.target.value, this.props.powerLevelKey);
- this.setState({ selectValue: event.target.value });
+ const powerLevel = parseInt(event.target.value);
+ this.props.onChange(powerLevel, this.props.powerLevelKey);
+ this.setState({ selectValue: powerLevel });
}
};
- onCustomChange = event => {
- this.setState({ customValue: event.target.value });
+ private onCustomChange = (event: React.ChangeEvent): void => {
+ this.setState({ customValue: parseInt(event.target.value) });
};
- onCustomBlur = event => {
+ private onCustomBlur = (event: React.FocusEvent): void => {
event.preventDefault();
event.stopPropagation();
- this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
+ this.props.onChange(this.state.customValue, this.props.powerLevelKey);
};
- onCustomKeyDown = event => {
+ private onCustomKeyDown = (event: React.KeyboardEvent): void => {
if (event.key === Key.ENTER) {
event.preventDefault();
event.stopPropagation();
@@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
// raising a dialog which causes a blur which causes a dialog which causes a blur and
// so on. By not causing the onChange to be called here, we avoid the loop because we
// handle the onBlur safely.
- event.target.blur();
+ (event.target as HTMLInputElement).blur();
}
};
- render() {
+ public render(): JSX.Element {
let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) {
@@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
);
} else {
// Each level must have a definition in this.state.levelRoleMap
- let options = this.state.options.map((level) => {
+ const options = this.state.options.map((level) => {
return {
- value: level,
+ value: String(level),
text: Roles.textualPowerLevel(level, this.props.usersDefault),
};
});
- options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
- options = options.map((op) => {
+ options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
+ const optionsElements = options.map((op) => {
return ;
});
@@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
value={String(this.state.selectValue)}
disabled={this.props.disabled}
>
- { options }
+ { optionsElements }
);
}
diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx
index d061d52f46..bd81218623 100644
--- a/src/components/views/elements/ReplyThread.tsx
+++ b/src/components/views/elements/ReplyThread.tsx
@@ -16,6 +16,8 @@ limitations under the License.
*/
import React from 'react';
+import classNames from 'classnames';
+
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
@@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile";
import Pill from './Pill';
import { Room } from 'matrix-js-sdk/src/models/room';
+/**
+ * This number is based on the previous behavior - if we have message of height
+ * over 60px then we want to show button that will allow to expand it.
+ */
+const SHOW_EXPAND_QUOTE_PIXELS = 60;
+
interface IProps {
// the latest event in this chain of replies
parentEv?: MatrixEvent;
@@ -45,6 +53,8 @@ interface IProps {
layout?: Layout;
// Whether to always show a timestamp
alwaysShowTimestamps?: boolean;
+ isQuoteExpanded?: boolean;
+ setQuoteExpanded: (isExpanded: boolean) => void;
}
interface IState {
@@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component {
static contextType = MatrixClientContext;
private unmounted = false;
private room: Room;
+ private blockquoteRef = React.createRef();
constructor(props, context) {
super(props, context);
@@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component {
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
}
- public static getParentEventId(ev: MatrixEvent): string {
+ public static getParentEventId(ev: MatrixEvent): string | undefined {
if (!ev || ev.isRedacted()) return;
// XXX: For newer relations (annotations, replacements, etc.), we now
@@ -88,7 +99,13 @@ export default class ReplyThread extends React.Component {
// could be used here for replies as well... However, the helper
// currently assumes the relation has a `rel_type`, which older replies
// do not, so this block is left as-is for now.
- const mRelatesTo = ev.getWireContent()['m.relates_to'];
+ //
+ // We're prefer ev.getContent() over ev.getWireContent() to make sure
+ // we grab the latest edit with potentially new relations. But we also
+ // can't just rely on ev.getContent() by itself because historically we
+ // still show the reply from the original message even though the edit
+ // event does not include the relation reply.
+ const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
const mInReplyTo = mRelatesTo['m.in_reply_to'];
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
@@ -131,7 +148,7 @@ export default class ReplyThread extends React.Component {
public static getNestedReplyText(
ev: MatrixEvent,
permalinkCreator: RoomPermalinkCreator,
- ): { body: string, html: string } {
+ ): { body: string, html: string } | null {
if (!ev) return null;
let { body, formatted_body: html } = ev.getContent();
@@ -231,37 +248,38 @@ export default class ReplyThread extends React.Component {
return replyMixin;
}
- public static makeThread(
- parentEv: MatrixEvent,
- onHeightChanged: () => void,
- permalinkCreator: RoomPermalinkCreator,
- ref: React.RefObject,
- layout: Layout,
- alwaysShowTimestamps: boolean,
- ): JSX.Element {
- if (!ReplyThread.getParentEventId(parentEv)) return null;
- return ;
+ public static hasThreadReply(event: MatrixEvent) {
+ return Boolean(ReplyThread.getParentEventId(event));
}
componentDidMount() {
this.initialize();
+ this.trySetExpandableQuotes();
}
componentDidUpdate() {
this.props.onHeightChanged();
+ this.trySetExpandableQuotes();
}
componentWillUnmount() {
this.unmounted = true;
}
+ private trySetExpandableQuotes() {
+ if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
+ const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
+ if (el) {
+ const code: HTMLElement | null = el.querySelector('code');
+ const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
+ const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
+ if (isElipsisShown) {
+ this.props.setQuoteExpanded(false);
+ }
+ }
+ }
+ }
+
private async initialize(): Promise {
const { parentEv } = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId
@@ -315,7 +333,7 @@ export default class ReplyThread extends React.Component {
this.initialize();
};
- private onQuoteClick = async (): Promise => {
+ private onQuoteClick = async (event: React.MouseEvent): Promise => {
const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null;
@@ -367,14 +385,26 @@ export default class ReplyThread extends React.Component {
header = ;
}
+ const { isQuoteExpanded } = this.props;
const evTiles = this.state.events.map((ev) => {
- return