@@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
@@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
+
diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
similarity index 94%
rename from src/components/views/dialogs/CryptoStoreTooNewDialog.js
rename to src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
index 6336c635e4..2bdf732bc5 100644
--- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js
+++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
@@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
-export default (props) => {
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
+
+export default (props: IProps) => {
const brand = SdkConfig.get().brand;
const _onLogoutClicked = () => {
@@ -40,7 +44,7 @@ export default (props) => {
onFinished: (doLogout) => {
if (doLogout) {
dis.dispatch({action: 'logout'});
- props.onFinished();
+ props.onFinished(true);
}
},
});
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.tsx
similarity index 87%
rename from src/components/views/dialogs/DeactivateAccountDialog.js
rename to src/components/views/dialogs/DeactivateAccountDialog.tsx
index 4e52549d51..cf88802340 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.js
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
@@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv
import StyledCheckbox from "../elements/StyledCheckbox";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
+
+interface IState {
+ shouldErase: boolean;
+ errStr: string;
+ authData: any; // for UIA
+ authEnabled: boolean; // see usages for information
+
+ // A few strings that are passed to InteractiveAuth for design or are displayed
+ // next to the InteractiveAuth component.
+ bodyText: string;
+ continueText: string;
+ continueKind: string;
+}
+
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
-export default class DeactivateAccountDialog extends React.Component {
+export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
@@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
continueKind: null,
};
- this._initAuth(/* shouldErase= */false);
+ this.initAuth(/* shouldErase= */false);
}
- _onStagePhaseChange = (stage, phase) => {
+ private onStagePhaseChange = (stage: string, phase: string): void => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
@@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component {
this.setState({bodyText, continueText, continueKind});
};
- _onUIAuthFinished = (success, result, extra) => {
+ private onUIAuthFinished = (success: boolean, result: Error) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
- this._onCancel();
+ this.onCancel();
return;
}
- console.error("Error during UI Auth:", {result, extra});
+ console.error("Error during UI Auth:", { result });
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
- _onUIAuthComplete = (auth) => {
+ private onUIAuthComplete = (auth: any): void => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
@@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component {
});
};
- _onEraseFieldChange = (ev) => {
+ private onEraseFieldChange = (ev: React.FormEvent): void => {
this.setState({
- shouldErase: ev.target.checked,
+ shouldErase: ev.currentTarget.checked,
// Disable the auth form because we're going to have to reinitialize the auth
// information. We do this because we can't modify the parameters in the UIA
@@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component {
});
// As mentioned above, set up for auth again to get updated UIA session info
- this._initAuth(/* shouldErase= */ev.target.checked);
+ this.initAuth(/* shouldErase= */ev.currentTarget.checked);
};
- _onCancel() {
+ private onCancel(): void {
this.props.onFinished(false);
}
- _initAuth(shouldErase) {
+ private initAuth(shouldErase: boolean): void {
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
@@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component {
});
}
- render() {
+ public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null;
@@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
@@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
{_t(
"Please forget all messages I have sent when my account is deactivated " +
@@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
);
}
}
-
-DeactivateAccountDialog.propTypes = {
- onFinished: PropTypes.func.isRequired,
-};
diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx
index fdbf6a36fc..2690eb67d7 100644
--- a/src/components/views/dialogs/DevtoolsDialog.tsx
+++ b/src/components/views/dialogs/DevtoolsDialog.tsx
@@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent {
diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.tsx
similarity index 81%
rename from src/components/views/dialogs/ErrorDialog.js
rename to src/components/views/dialogs/ErrorDialog.tsx
index 5197c68b5a..d50ec7bf36 100644
--- a/src/components/views/dialogs/ErrorDialog.js
+++ b/src/components/views/dialogs/ErrorDialog.tsx
@@ -26,37 +26,37 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
-@replaceableComponent("views.dialogs.ErrorDialog")
-export default class ErrorDialog extends React.Component {
- static propTypes = {
- title: PropTypes.string,
- description: PropTypes.oneOfType([
- PropTypes.element,
- PropTypes.string,
- ]),
- button: PropTypes.string,
- focus: PropTypes.bool,
- onFinished: PropTypes.func.isRequired,
- headerImage: PropTypes.string,
- };
+interface IProps {
+ onFinished: (success: boolean) => void;
+ title?: string;
+ description?: React.ReactNode;
+ button?: string;
+ focus?: boolean;
+ headerImage?: string;
+}
- static defaultProps = {
+interface IState {
+ onFinished: (success: boolean) => void;
+}
+
+@replaceableComponent("views.dialogs.ErrorDialog")
+export default class ErrorDialog extends React.Component {
+ public static defaultProps = {
focus: true,
title: null,
description: null,
button: null,
};
- onClick = () => {
+ private onClick = () => {
this.props.onFinished(true);
};
- render() {
+ public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
= ({ matrixClient: cli, event, permalinkCr
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
+ rawDisplayName: profileInfo.displayname,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 778744b783..ffca9a88a7 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -153,8 +153,8 @@ class ThreepidMember extends Member {
}
interface IDMUserTileProps {
- member: RoomMember;
- onRemove(member: RoomMember): void;
+ member: Member;
+ onRemove(member: Member): void;
}
class DMUserTile extends React.PureComponent {
@@ -168,7 +168,7 @@ class DMUserTile extends React.PureComponent {
render() {
const avatarSize = 20;
- const avatar = this.props.member.isEmail
+ const avatar = (this.props.member as ThreepidMember).isEmail
?
{
}
interface IDMRoomTileProps {
- member: RoomMember;
+ member: Member;
lastActiveTs: number;
- onToggle(member: RoomMember): void;
+ onToggle(member: Member): void;
highlightWord: string;
isSelected: boolean;
}
@@ -270,7 +270,7 @@ class DMRoomTile extends React.PureComponent {
}
const avatarSize = 36;
- const avatar = this.props.member.isEmail
+ const avatar = (this.props.member as ThreepidMember).isEmail
?
@@ -298,7 +298,7 @@ class DMRoomTile extends React.PureComponent {
);
- const caption = this.props.member.isEmail
+ const caption = (this.props.member as ThreepidMember).isEmail
? _t("Invite by email")
: this.highlightName(this.props.member.userId);
@@ -334,7 +334,7 @@ interface IInviteDialogProps {
}
interface IInviteDialogState {
- targets: RoomMember[]; // array of Member objects (see interface above)
+ targets: Member[]; // array of Member objects (see interface above)
filterText: string;
recents: { user: Member, userId: string }[];
numRecentsShown: number;
diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js
deleted file mode 100644
index 5454b97287..0000000000
--- a/src/components/views/dialogs/ReportEventDialog.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, {PureComponent} from 'react';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
-import PropTypes from "prop-types";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import SdkConfig from '../../../SdkConfig';
-import Markdown from '../../../Markdown';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-/*
- * A dialog for reporting an event.
- */
-@replaceableComponent("views.dialogs.ReportEventDialog")
-export default class ReportEventDialog extends PureComponent {
- static propTypes = {
- mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
- onFinished: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- reason: "",
- busy: false,
- err: null,
- };
- }
-
- _onReasonChange = ({target: {value: reason}}) => {
- this.setState({ reason });
- };
-
- _onCancel = () => {
- this.props.onFinished(false);
- };
-
- _onSubmit = async () => {
- if (!this.state.reason || !this.state.reason.trim()) {
- this.setState({
- err: _t("Please fill why you're reporting."),
- });
- return;
- }
-
- this.setState({
- busy: true,
- err: null,
- });
-
- try {
- const ev = this.props.mxEvent;
- await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
- this.props.onFinished(true);
- } catch (e) {
- this.setState({
- busy: false,
- err: e.message,
- });
- }
- };
-
- render() {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- const Loader = sdk.getComponent('elements.Spinner');
- const Field = sdk.getComponent('elements.Field');
-
- let error = null;
- if (this.state.err) {
- error =
- {this.state.err}
-
;
- }
-
- let progress = null;
- if (this.state.busy) {
- progress = (
-
-
-
- );
- }
-
- const adminMessageMD =
- SdkConfig.get().reportEvent &&
- SdkConfig.get().reportEvent.adminMessageMD;
- let adminMessage;
- if (adminMessageMD) {
- const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
- adminMessage =
;
- }
-
- return (
-
-
-
- {
- _t("Reporting this message will send its unique 'event ID' to the administrator of " +
- "your homeserver. If messages in this room are encrypted, your homeserver " +
- "administrator will not be able to read the message text or view any files or images.")
- }
-
- {adminMessage}
-
- {progress}
- {error}
-
-
-
- );
- }
-}
diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx
new file mode 100644
index 0000000000..8271239f7f
--- /dev/null
+++ b/src/components/views/dialogs/ReportEventDialog.tsx
@@ -0,0 +1,445 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import * as sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+import { ensureDMExists } from "../../../createRoom";
+import { IDialogProps } from "./IDialogProps";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import SdkConfig from '../../../SdkConfig';
+import Markdown from '../../../Markdown';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
+import SettingsStore from "../../../settings/SettingsStore";
+import StyledRadioButton from "../elements/StyledRadioButton";
+
+interface IProps extends IDialogProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ // A free-form text describing the abuse.
+ reason: string;
+ busy: boolean;
+ err?: string;
+ // If we know it, the nature of the abuse, as specified by MSC3215.
+ nature?: EXTENDED_NATURE;
+}
+
+
+const MODERATED_BY_STATE_EVENT_TYPE = [
+ "org.matrix.msc3215.room.moderation.moderated_by",
+ /**
+ * Unprefixed state event. Not ready for prime time.
+ *
+ * "m.room.moderation.moderated_by"
+ */
+];
+
+const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
+
+// Standard abuse natures.
+enum NATURE {
+ DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
+ TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
+ ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
+ SPAM = "org.matrix.msc3215.abuse.nature.spam",
+ OTHER = "org.matrix.msc3215.abuse.nature.other",
+}
+
+enum NON_STANDARD_NATURE {
+ // Non-standard abuse nature.
+ // It should never leave the client - we use it to fallback to
+ // server-wide abuse reporting.
+ ADMIN = "non-standard.abuse.nature.admin"
+}
+
+type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
+
+type Moderation = {
+ // The id of the moderation room.
+ moderationRoomId: string;
+ // The id of the bot in charge of forwarding abuse reports to the moderation room.
+ moderationBotUserId: string;
+}
+/*
+ * A dialog for reporting an event.
+ *
+ * The actual content of the dialog will depend on two things:
+ *
+ * 1. Is `feature_report_to_moderators` enabled?
+ * 2. Does the room support moderation as per MSC3215, i.e. is there
+ * a well-formed state event `m.room.moderation.moderated_by`
+ * /`org.matrix.msc3215.room.moderation.moderated_by`?
+ */
+@replaceableComponent("views.dialogs.ReportEventDialog")
+export default class ReportEventDialog extends React.Component
{
+ // If the room supports moderation, the moderation information.
+ private moderation?: Moderation;
+
+ constructor(props: IProps) {
+ super(props);
+
+ let moderatedByRoomId = null;
+ let moderatedByUserId = null;
+
+ if (SettingsStore.getValue("feature_report_to_moderators")) {
+ // The client supports reporting to moderators.
+ // Does the room support it, too?
+
+ // Extract state events to determine whether we should display
+ const client = MatrixClientPeg.get();
+ const room = client.getRoom(props.mxEvent.getRoomId());
+
+ for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
+ const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
+ if (!stateEvent) {
+ continue;
+ }
+ if (Array.isArray(stateEvent)) {
+ // Internal error.
+ throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
+ "should return at most one state event");
+ }
+ const event = stateEvent.event;
+ if (!("content" in event) || typeof event["content"] != "object") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have an object field `content`, got", event);
+ continue;
+ }
+ const content = event["content"];
+ if (!("room_id" in content) || typeof content["room_id"] != "string") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have a string field `content.room_id`, got", event);
+ continue;
+ }
+ if (!("user_id" in content) || typeof content["user_id"] != "string") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have a string field `content.user_id`, got", event);
+ continue;
+ }
+ moderatedByRoomId = content["room_id"];
+ moderatedByUserId = content["user_id"];
+ }
+
+ if (moderatedByRoomId && moderatedByUserId) {
+ // The room supports moderation.
+ this.moderation = {
+ moderationRoomId: moderatedByRoomId,
+ moderationBotUserId: moderatedByUserId,
+ };
+ }
+ }
+
+ this.state = {
+ // A free-form text describing the abuse.
+ reason: "",
+ busy: false,
+ err: null,
+ // If specified, the nature of the abuse, as specified by MSC3215.
+ nature: null,
+ };
+ }
+
+ // The user has written down a freeform description of the abuse.
+ private onReasonChange = ({target: {value: reason}}): void => {
+ this.setState({ reason });
+ };
+
+ // The user has clicked on a nature.
+ private onNatureChosen = (e: React.FormEvent): void => {
+ this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
+ };
+
+ // The user has clicked "cancel".
+ private onCancel = (): void => {
+ this.props.onFinished(false);
+ };
+
+ // The user has clicked "submit".
+ private onSubmit = async () => {
+ let reason = this.state.reason || "";
+ reason = reason.trim();
+ if (this.moderation) {
+ // This room supports moderation.
+ // We need a nature.
+ // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
+ if (!this.state.nature ||
+ ((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
+ && !reason)
+ ) {
+ this.setState({
+ err: _t("Please fill why you're reporting."),
+ });
+ return;
+ }
+ } else {
+ // This room does not support moderation.
+ // We need a `reason`.
+ if (!reason) {
+ this.setState({
+ err: _t("Please fill why you're reporting."),
+ });
+ return;
+ }
+ }
+
+ this.setState({
+ busy: true,
+ err: null,
+ });
+
+ try {
+ const client = MatrixClientPeg.get();
+ const ev = this.props.mxEvent;
+ if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
+ const nature: NATURE = this.state.nature;
+
+ // Report to moderators through to the dedicated bot,
+ // as configured in the room's state events.
+ const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
+ await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
+ event_id: ev.getId(),
+ room_id: ev.getRoomId(),
+ moderated_by_id: this.moderation.moderationRoomId,
+ nature,
+ reporter: client.getUserId(),
+ comment: this.state.reason.trim(),
+ });
+ } else {
+ // Report to homeserver admin through the dedicated Matrix API.
+ await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
+ }
+ this.props.onFinished(true);
+ } catch (e) {
+ this.setState({
+ busy: false,
+ err: e.message,
+ });
+ }
+ };
+
+ render() {
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ const Loader = sdk.getComponent('elements.Spinner');
+ const Field = sdk.getComponent('elements.Field');
+
+ let error = null;
+ if (this.state.err) {
+ error =
+ {this.state.err}
+
;
+ }
+
+ let progress = null;
+ if (this.state.busy) {
+ progress = (
+
+
+
+ );
+ }
+
+ const adminMessageMD =
+ SdkConfig.get().reportEvent &&
+ SdkConfig.get().reportEvent.adminMessageMD;
+ let adminMessage;
+ if (adminMessageMD) {
+ const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
+ adminMessage = ;
+ }
+
+ if (this.moderation) {
+ // Display report-to-moderator dialog.
+ // We let the user pick a nature.
+ const client = MatrixClientPeg.get();
+ const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
+ let subtitle;
+ switch (this.state.nature) {
+ case NATURE.DISAGREEMENT:
+ subtitle = _t("What this user is writing is wrong.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NATURE.TOXIC:
+ subtitle = _t("This user is displaying toxic behaviour, " +
+ "for instance by insulting other users or sharing " +
+ " adult-only content in a family-friendly room " +
+ " or otherwise violating the rules of this room.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NATURE.ILLEGAL:
+ subtitle = _t("This user is displaying illegal behaviour, " +
+ "for instance by doxing people or threatening violence.\n" +
+ "This will be reported to the room moderators who may escalate this to legal authorities.");
+ break;
+ case NATURE.SPAM:
+ subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NON_STANDARD_NATURE.ADMIN:
+ if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
+ subtitle = _t("This room is dedicated to illegal or toxic content " +
+ "or the moderators fail to moderate illegal or toxic content.\n" +
+ "This will be reported to the administrators of %(homeserver)s. " +
+ "The administrators will NOT be able to read the encrypted content of this room.",
+ { homeserver: homeServerName });
+ } else {
+ subtitle = _t("This room is dedicated to illegal or toxic content " +
+ "or the moderators fail to moderate illegal or toxic content.\n" +
+ " This will be reported to the administrators of %(homeserver)s.",
+ { homeserver: homeServerName });
+ }
+ break;
+ case NATURE.OTHER:
+ subtitle = _t("Any other reason. Please describe the problem.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ default:
+ subtitle = _t("Please pick a nature and describe what makes this message abusive.");
+ break;
+ }
+
+ return (
+
+
+
+ {_t('Disagree')}
+
+
+ {_t('Toxic Behaviour')}
+
+
+ {_t('Illegal Content')}
+
+
+ {_t('Spam or propaganda')}
+
+
+ {_t('Report the entire room')}
+
+
+ {_t('Other')}
+
+
+ {subtitle}
+
+
+ {progress}
+ {error}
+
+
+
+ );
+ }
+ // Report to homeserver admin.
+ // Currently, the API does not support natures.
+ return (
+
+
+
+ {
+ _t("Reporting this message will send its unique 'event ID' to the administrator of " +
+ "your homeserver. If messages in this room are encrypted, your homeserver " +
+ "administrator will not be able to read the message text or view any files " +
+ "or images.")
+ }
+
+ {adminMessage}
+
+ {progress}
+ {error}
+
+
+
+ );
+ }
+}
diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.tsx
similarity index 72%
rename from src/components/views/dialogs/TermsDialog.js
rename to src/components/views/dialogs/TermsDialog.tsx
index e8625ec6cb..ace5316323 100644
--- a/src/components/views/dialogs/TermsDialog.js
+++ b/src/components/views/dialogs/TermsDialog.tsx
@@ -16,22 +16,21 @@ limitations under the License.
import url from 'url';
import React from 'react';
-import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
-import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
+import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
-class TermsCheckbox extends React.PureComponent {
- static propTypes = {
- onChange: PropTypes.func.isRequired,
- url: PropTypes.string.isRequired,
- checked: PropTypes.bool.isRequired,
- }
+interface ITermsCheckboxProps {
+ onChange: (url: string, checked: boolean) => void;
+ url: string;
+ checked: boolean;
+}
- onChange = (ev) => {
- this.props.onChange(this.props.url, ev.target.checked);
+class TermsCheckbox extends React.PureComponent {
+ private onChange = (ev: React.FormEvent): void => {
+ this.props.onChange(this.props.url, ev.currentTarget.checked);
}
render() {
@@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
}
}
+interface ITermsDialogProps {
+ /**
+ * Array of [Service, policies] pairs, where policies is the response from the
+ * /terms endpoint for that service
+ */
+ policiesAndServicePairs: any[],
+
+ /**
+ * urls that the user has already agreed to
+ */
+ agreedUrls?: string[],
+
+ /**
+ * Called with:
+ * * success {bool} True if the user accepted any douments, false if cancelled
+ * * agreedUrls {string[]} List of agreed URLs
+ */
+ onFinished: (success: boolean, agreedUrls?: string[]) => void,
+}
+
+interface IState {
+ agreedUrls: any;
+}
+
@replaceableComponent("views.dialogs.TermsDialog")
-export default class TermsDialog extends React.PureComponent {
- static propTypes = {
- /**
- * Array of [Service, policies] pairs, where policies is the response from the
- * /terms endpoint for that service
- */
- policiesAndServicePairs: PropTypes.array.isRequired,
-
- /**
- * urls that the user has already agreed to
- */
- agreedUrls: PropTypes.arrayOf(PropTypes.string),
-
- /**
- * Called with:
- * * success {bool} True if the user accepted any douments, false if cancelled
- * * agreedUrls {string[]} List of agreed URLs
- */
- onFinished: PropTypes.func.isRequired,
- }
-
+export default class TermsDialog extends React.PureComponent {
constructor(props) {
- super();
+ super(props);
this.state = {
// url -> boolean
agreedUrls: {},
@@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
}
}
- _onCancelClick = () => {
+ private onCancelClick = (): void => {
this.props.onFinished(false);
}
- _onNextClick = () => {
+ private onNextClick = (): void => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
}
- _nameForServiceType(serviceType, host) {
+ private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
switch (serviceType) {
case SERVICE_TYPES.IS:
return {_t("Identity Server")}
({host})
;
@@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
}
}
- _summaryForServiceType(serviceType) {
+ private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
switch (serviceType) {
case SERVICE_TYPES.IS:
return
@@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
}
}
- _onTermsCheckboxChange = (url, checked) => {
+ private onTermsCheckboxChange = (url: string, checked: boolean) => {
this.setState({
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
});
}
- render() {
+ public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
let serviceName;
let summary;
if (i === 0) {
- serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
- summary = this._summaryForServiceType(
+ serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
+ summary = this.summaryForServiceType(
policiesAndService.service.serviceType,
);
}
@@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
rows.push(
{serviceName} |
{summary} |
- {termDoc[termsLang].name}
-
- |
+
+ {termDoc[termsLang].name}
+
+
+
+ |
|
);
@@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
return (
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.tsx
similarity index 75%
rename from src/components/views/dialogs/UserSettingsDialog.js
rename to src/components/views/dialogs/UserSettingsDialog.tsx
index fe29b85aea..1a62a4ff22 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.tsx
@@ -16,11 +16,10 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import TabbedView, {Tab} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
-import SettingsStore from "../../../settings/SettingsStore";
+import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
@@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
-export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
-export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
-export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
-export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
-export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
-export const USER_VOICE_TAB = "USER_VOICE_TAB";
-export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
-export const USER_LABS_TAB = "USER_LABS_TAB";
-export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
-export const USER_HELP_TAB = "USER_HELP_TAB";
+export enum UserTab {
+ General = "USER_GENERAL_TAB",
+ Appearance = "USER_APPEARANCE_TAB",
+ Flair = "USER_FLAIR_TAB",
+ Notifications = "USER_NOTIFICATIONS_TAB",
+ Preferences = "USER_PREFERENCES_TAB",
+ Voice = "USER_VOICE_TAB",
+ Security = "USER_SECURITY_TAB",
+ Labs = "USER_LABS_TAB",
+ Mjolnir = "USER_MJOLNIR_TAB",
+ Help = "USER_HELP_TAB",
+}
+
+interface IProps {
+ onFinished: (success: boolean) => void;
+ initialTabId?: string;
+}
+
+interface IState {
+ mjolnirEnabled: boolean;
+}
@replaceableComponent("views.dialogs.UserSettingsDialog")
-export default class UserSettingsDialog extends React.Component {
- static propTypes = {
- onFinished: PropTypes.func.isRequired,
- initialTabId: PropTypes.string,
- };
+export default class UserSettingsDialog extends React.Component
{
+ private mjolnirWatcher: string;
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.state = {
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
};
}
- componentDidMount(): void {
- this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
+ public componentDidMount(): void {
+ this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
}
- componentWillUnmount(): void {
- SettingsStore.unwatchSetting(this._mjolnirWatcher);
+ public componentWillUnmount(): void {
+ SettingsStore.unwatchSetting(this.mjolnirWatcher);
}
- _mjolnirChanged(settingName, roomId, atLevel, newValue) {
+ private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({mjolnirEnabled: newValue});
}
@@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
- USER_GENERAL_TAB,
+ UserTab.General,
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
,
));
tabs.push(new Tab(
- USER_APPEARANCE_TAB,
+ UserTab.Appearance,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
,
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
- USER_FLAIR_TAB,
+ UserTab.Flair,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
,
));
}
tabs.push(new Tab(
- USER_NOTIFICATIONS_TAB,
+ UserTab.Notifications,
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
,
));
tabs.push(new Tab(
- USER_PREFERENCES_TAB,
+ UserTab.Preferences,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
,
@@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab(
- USER_VOICE_TAB,
+ UserTab.Voice,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
,
@@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component {
}
tabs.push(new Tab(
- USER_SECURITY_TAB,
+ UserTab.Security,
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
,
@@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component {
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
) {
tabs.push(new Tab(
- USER_LABS_TAB,
+ UserTab.Labs,
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
,
@@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
- USER_MJOLNIR_TAB,
+ UserTab.Mjolnir,
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
,
));
}
tabs.push(new Tab(
- USER_HELP_TAB,
+ UserTab.Help,
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
- ,
+ this.props.onFinished(true)} />,
));
return tabs;
diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
similarity index 83%
rename from src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
rename to src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
index e71983b074..6272302a76 100644
--- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
@@ -15,22 +15,21 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from "../../../../languageHandler";
+import { _t } from "../../../../languageHandler";
import * as sdk from "../../../../index";
-import {replaceableComponent} from "../../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../../utils/replaceableComponent";
+
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
-export default class ConfirmDestroyCrossSigningDialog extends React.Component {
- static propTypes = {
- onFinished: PropTypes.func.isRequired,
- };
-
- _onConfirm = () => {
+export default class ConfirmDestroyCrossSigningDialog extends React.Component {
+ private onConfirm = (): void => {
this.props.onFinished(true);
};
- _onDecline = () => {
+ private onDecline = (): void => {
this.props.onFinished(false);
};
@@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
);
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
similarity index 85%
rename from src/components/views/dialogs/security/CreateCrossSigningDialog.js
rename to src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index fedcc02f89..840390f6fb 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
@@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog';
-import {replaceableComponent} from "../../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../../utils/replaceableComponent";
+
+interface IProps {
+ accountPassword?: string;
+ tokenLogin?: boolean;
+ onFinished?: (success: boolean) => void;
+}
+
+interface IState {
+ error: Error | null;
+ canUploadKeysWithPasswordOnly?: boolean;
+ accountPassword: string;
+}
/*
* Walks the user through the process of creating a cross-signing keys. In most
@@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent";
* may need to complete some steps to proceed.
*/
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
-export default class CreateCrossSigningDialog extends React.PureComponent {
- static propTypes = {
- accountPassword: PropTypes.string,
- tokenLogin: PropTypes.bool,
- };
-
- constructor(props) {
+export default class CreateCrossSigningDialog extends React.PureComponent {
+ constructor(props: IProps) {
super(props);
this.state = {
error: null,
// Does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
- canUploadKeysWithPasswordOnly: null,
- accountPassword: props.accountPassword || "",
- };
-
- if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
- this.state.canUploadKeysWithPasswordOnly = true;
- } else {
- this._queryKeyUploadAuth();
+ canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
+ accountPassword: props.accountPassword || "",
+ };
+
+ if (!this.state.accountPassword) {
+ this.queryKeyUploadAuth();
}
}
- componentDidMount() {
- this._bootstrapCrossSigning();
+ public componentDidMount(): void {
+ this.bootstrapCrossSigning();
}
- async _queryKeyUploadAuth() {
+ private async queryKeyUploadAuth(): Promise {
try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require
@@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
}
}
- _doBootstrapUIAuth = async (makeRequest) => {
+ private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: 'm.login.password',
@@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
}
}
- _bootstrapCrossSigning = async () => {
+ private bootstrapCrossSigning = async (): Promise => {
this.setState({
error: null,
});
@@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
try {
await cli.bootstrapCrossSigning({
- authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
+ authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
});
this.props.onFinished(true);
} catch (e) {
if (this.props.tokenLogin) {
// ignore any failures, we are relying on grace period here
- this.props.onFinished();
+ this.props.onFinished(false);
return;
}
@@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
}
}
- _onCancel = () => {
+ private onCancel = (): void => {
this.props.onFinished(false);
}
@@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
{_t("Unable to set up keys")}
;
diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx
similarity index 72%
rename from src/components/views/dialogs/security/SetupEncryptionDialog.js
rename to src/components/views/dialogs/security/SetupEncryptionDialog.tsx
index 3c15ea9f1d..19c7af01ff 100644
--- a/src/components/views/dialogs/security/SetupEncryptionDialog.js
+++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx
@@ -15,47 +15,52 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler';
-import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
+import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
import {replaceableComponent} from "../../../../utils/replaceableComponent";
-function iconFromPhase(phase) {
- if (phase === PHASE_DONE) {
+function iconFromPhase(phase: Phase) {
+ if (phase === Phase.Done) {
return require("../../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../../res/img/e2e/warning.svg");
}
}
-@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
-export default class SetupEncryptionDialog extends React.Component {
- static propTypes = {
- onFinished: PropTypes.func.isRequired,
- };
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
- constructor() {
- super();
+interface IState {
+ icon: Phase;
+}
+
+@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
+export default class SetupEncryptionDialog extends React.Component