): 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/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
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
- };
+ } as RoomMember;
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
@@ -195,6 +199,17 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr
}).match(lcQuery);
}
+ const [truncateAt, setTruncateAt] = useState(20);
+ function overflowTile(overflowCount, totalCount) {
+ const text = _t("and %(count)s others...", { count: overflowCount });
+ return (
+
+ } name={text} presenceState="online" suppressOnHover={true}
+ onClick={() => setTruncateAt(totalCount)} />
+ );
+ }
+
return = ({ matrixClient: cli, event, permalinkCr
{ rooms.length > 0 ? (
- { rooms.map(room =>
- ,
- ) }
+ rooms.slice(start, end).map(room =>
+ ,
+ )}
+ getChildCount={() => rooms.length}
+ />
) :
{ _t("No results") }
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/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 1a664951c5..303f17c342 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component {
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
- ,
+ this.props.onFinished(true)}
+ />,
));
}
diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx
index a135b6bc16..5e0cd96740 100644
--- a/src/components/views/dialogs/SpaceSettingsDialog.tsx
+++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx
@@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useState} from 'react';
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import React, { useMemo } from 'react';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
-import {_t} from '../../../languageHandler';
-import {IDialogProps} from "./IDialogProps";
+import { _t, _td } from '../../../languageHandler';
+import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
-import DevtoolsDialog from "./DevtoolsDialog";
-import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
-import {getTopic} from "../elements/RoomTopic";
-import {avatarUrlForRoom} from "../../../Avatar";
-import ToggleSwitch from "../elements/ToggleSwitch";
-import AccessibleButton from "../elements/AccessibleButton";
-import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {useDispatcher} from "../../../hooks/useDispatcher";
-import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
+import { useDispatcher } from "../../../hooks/useDispatcher";
+import TabbedView, { Tab } from "../../structures/TabbedView";
+import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
+import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
+import SettingsStore from "../../../settings/SettingsStore";
+import { UIFeature } from "../../../settings/UIFeature";
+import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
+
+export enum SpaceSettingsTab {
+ General = "SPACE_GENERAL_TAB",
+ Visibility = "SPACE_VISIBILITY_TAB",
+ Advanced = "SPACE_ADVANCED_TAB",
+}
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin
}
});
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState("");
-
- const userId = cli.getUserId();
-
- const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
- const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
- const avatarChanged = newAvatar !== null;
-
- const [name, setName] = useState(space.name);
- const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
- const nameChanged = name !== space.name;
-
- const currentTopic = getTopic(space);
- const [topic, setTopic] = useState(currentTopic);
- const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
- const topicChanged = topic !== currentTopic;
-
- const currentJoinRule = space.getJoinRule();
- const [joinRule, setJoinRule] = useState(currentJoinRule);
- const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
- const joinRuleChanged = joinRule !== currentJoinRule;
-
- const onSave = async () => {
- setBusy(true);
- const promises = [];
-
- if (avatarChanged) {
- if (newAvatar) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
- url: await cli.uploadContent(newAvatar),
- }, ""));
- } else {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
- }
- }
-
- if (nameChanged) {
- promises.push(cli.setRoomName(space.roomId, name));
- }
-
- if (topicChanged) {
- promises.push(cli.setRoomTopic(space.roomId, topic));
- }
-
- if (joinRuleChanged) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
- }
-
- const results = await Promise.allSettled(promises);
- setBusy(false);
- const failures = results.filter(r => r.status === "rejected");
- if (failures.length > 0) {
- console.error("Failed to save space settings: ", failures);
- setError(_t("Failed to save space settings."));
- }
- };
+ const tabs = useMemo(() => {
+ return [
+ new Tab(
+ SpaceSettingsTab.General,
+ _td("General"),
+ "mx_SpaceSettingsDialog_generalIcon",
+ ,
+ ),
+ new Tab(
+ SpaceSettingsTab.Visibility,
+ _td("Visibility"),
+ "mx_SpaceSettingsDialog_visibilityIcon",
+ ,
+ ),
+ SettingsStore.getValue(UIFeature.AdvancedSettings)
+ ? new Tab(
+ SpaceSettingsTab.Advanced,
+ _td("Advanced"),
+ "mx_RoomSettingsDialog_warningIcon",
+ ,
+ )
+ : null,
+ ].filter(Boolean);
+ }, [cli, space, onFinished]);
return = ({ matrixClient: cli, space, onFin
onFinished={onFinished}
fixedWidth={false}
>
-
-
{ _t("Edit settings relating to your space.") }
-
- { error &&
{ error }
}
-
-
onFinished(false)} />
-
-
-
-
- { _t("Make this space private") }
- setJoinRule(checked ? "invite" : "public")}
- disabled={!canSetJoinRule}
- aria-label={_t("Make this space private")}
- />
-
-
- {
- defaultDispatcher.dispatch({
- action: "leave_room",
- room_id: space.roomId,
- });
- }}
- >
- { _t("Leave Space") }
-
-
-
-
Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
- { _t("View dev tools") }
-
-
- { _t("Cancel") }
-
-
- { busy ? _t("Saving...") : _t("Save Changes") }
-
-
+
+
;
};
export default SpaceSettingsDialog;
-
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 {
+ private store: SetupEncryptionStore;
+
+ constructor(props: IProps) {
+ super(props);
this.store = SetupEncryptionStore.sharedInstance();
this.state = {icon: iconFromPhase(this.store.phase)};
}
- componentDidMount() {
- this.store.on("update", this._onStoreUpdate);
+ public componentDidMount() {
+ this.store.on("update", this.onStoreUpdate);
}
- componentWillUnmount() {
- this.store.removeListener("update", this._onStoreUpdate);
+ public componentWillUnmount() {
+ this.store.removeListener("update", this.onStoreUpdate);
}
- _onStoreUpdate = () => {
+ private onStoreUpdate = (): void => {
this.setState({icon: iconFromPhase(this.store.phase)});
};
- render() {
+ public render() {
return {
@@ -94,6 +98,8 @@ export default function AccessibleButton({
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
+ } else {
+ onKeyUp?.(e);
}
};
}
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index 67572d4508..2e88d37882 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -18,7 +18,6 @@ limitations under the License.
import TagTile from './TagTile';
import React from 'react';
-import { Draggable } from 'react-beautiful-dnd';
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index';
@@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
-
+
);
}
- return
-
- {(provided, snapshot) => (
-
-
-
- )}
-
+ return <>
+
{contextMenu}
-
;
+ >;
}
diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx
index fd1c7848aa..426554f31e 100644
--- a/src/components/views/elements/DesktopBuildsNotice.tsx
+++ b/src/components/views/elements/DesktopBuildsNotice.tsx
@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React from "react";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
-import React from "react";
+import dis from "../../../dispatcher/dispatcher";
+import { Action } from "../../../dispatcher/actions";
+import { UserTab } from "../dialogs/UserSettingsDialog";
+
export enum WarningKind {
Files,
@@ -33,6 +37,22 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
if (!isRoomEncrypted) return null;
if (EventIndexPeg.get()) return null;
+ if (EventIndexPeg.error) {
+ return <>
+ {_t("Message search initialisation failed, check your settings for more information", {}, {
+ a: sub => ( {
+ evt.preventDefault();
+ dis.dispatch({
+ action: Action.ViewUserSettings,
+ initialTabId: UserTab.Security,
+ });
+ }}>
+ {sub}
+ ),
+ })}
+ >;
+ }
+
const {desktopBuilds, brand} = SdkConfig.get();
let text = null;
diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.tsx
similarity index 54%
rename from src/components/views/elements/EditableItemList.js
rename to src/components/views/elements/EditableItemList.tsx
index d8ec5af278..89e2e1b8a0 100644
--- a/src/components/views/elements/EditableItemList.js
+++ b/src/components/views/elements/EditableItemList.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2017, 2019 New Vector Ltd.
+Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,48 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from '../../../languageHandler';
+import React from "react";
+
+import { _t } from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
-export class EditableItem extends React.Component {
- static propTypes = {
- index: PropTypes.number,
- value: PropTypes.string,
- onRemove: PropTypes.func,
+interface IItemProps {
+ index?: number;
+ value?: string;
+ onRemove?(index: number): void;
+}
+
+interface IItemState {
+ verifyRemove: boolean;
+}
+
+export class EditableItem extends React.Component {
+ public state = {
+ verifyRemove: false,
};
- constructor() {
- super();
-
- this.state = {
- verifyRemove: false,
- };
- }
-
- _onRemove = (e) => {
+ private onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
- this.setState({verifyRemove: true});
+ this.setState({ verifyRemove: true });
};
- _onDontRemove = (e) => {
+ private onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
- this.setState({verifyRemove: false});
+ this.setState({ verifyRemove: false });
};
- _onActuallyRemove = (e) => {
+ private onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onRemove) this.props.onRemove(this.props.index);
- this.setState({verifyRemove: false});
+ this.setState({ verifyRemove: false });
};
render() {
@@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
{_t("Are you sure?")}
{_t("Yes")}
@@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
return (
);
}
}
+interface IProps {
+ id: string;
+ items: string[];
+ itemsLabel?: string;
+ noItemsLabel?: string;
+ placeholder?: string;
+ newItem?: string;
+ canEdit?: boolean;
+ canRemove?: boolean;
+ suggestionsListId?: string;
+ onItemAdded?(item: string): void;
+ onItemRemoved?(index: number): void;
+ onNewItemChanged?(item: string): void;
+}
+
@replaceableComponent("views.elements.EditableItemList")
-export default class EditableItemList extends React.Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- items: PropTypes.arrayOf(PropTypes.string).isRequired,
- itemsLabel: PropTypes.string,
- noItemsLabel: PropTypes.string,
- placeholder: PropTypes.string,
- newItem: PropTypes.string,
-
- onItemAdded: PropTypes.func,
- onItemRemoved: PropTypes.func,
- onNewItemChanged: PropTypes.func,
-
- canEdit: PropTypes.bool,
- canRemove: PropTypes.bool,
- };
-
- _onItemAdded = (e) => {
+export default class EditableItemList extends React.PureComponent {
+ protected onItemAdded = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
};
- _onItemRemoved = (index) => {
+ protected onItemRemoved = (index) => {
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
};
- _onNewItemChanged = (e) => {
+ protected onNewItemChanged = (e) => {
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
};
- _renderNewItemField() {
+ protected renderNewItemField() {
return (
);
@@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
key={item}
index={index}
value={item}
- onRemove={this._onItemRemoved}
+ onRemove={this.onItemRemoved}
/>;
});
const editableItemsSection = this.props.canRemove ? editableItems : ;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
- return (
-
- { label }
+ return (
+
+
+ { label }
+
+ { editableItemsSection }
+ { this.props.canEdit ? this.renderNewItemField() :
}
- { editableItemsSection }
- { this.props.canEdit ? this._renderNewItemField() :
}
-
);
+ );
}
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index cf3b7a6e61..332f3ac333 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { Layout } from "../../../settings/Layout";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/**
@@ -101,7 +102,8 @@ export default class EventTilePreview extends React.Component
{
// Fake it more
event.sender = {
- name: this.props.displayName,
+ name: this.props.displayName || this.props.userId,
+ rawDisplayName: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
@@ -110,7 +112,7 @@ export default class EventTilePreview extends React.Component {
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
- };
+ } as RoomMember;
return event;
}
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 59d9a11596..1373c2df0e 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -29,6 +29,11 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
+export interface IValidateOpts {
+ focused?: boolean;
+ allowEmpty?: boolean;
+}
+
interface IProps {
// The field's ID, which binds the input and label together. Immutable.
id?: string;
@@ -180,7 +185,7 @@ export default class Field extends React.PureComponent {
}
};
- public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
+ public async validate({ focused, allowEmpty = true }: IValidateOpts) {
if (!this.props.onValidate) {
return;
}
diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js
deleted file mode 100644
index f6b4c986f5..0000000000
--- a/src/components/views/elements/FormButton.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import AccessibleButton from "./AccessibleButton";
-
-export default function FormButton(props) {
- const {className, label, kind, ...restProps} = props;
- const newClassName = (className || "") + " mx_FormButton";
- const allProps = Object.assign({}, restProps,
- {className: newClassName, kind: kind || "primary", children: [label]});
- return React.createElement(AccessibleButton, allProps);
-}
-
-FormButton.propTypes = AccessibleButton.propTypes;
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx
similarity index 63%
rename from src/components/views/elements/LabelledToggleSwitch.js
rename to src/components/views/elements/LabelledToggleSwitch.tsx
index ef60eeed7b..d97b698fd8 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,38 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from "prop-types";
+import React from "react";
+
import ToggleSwitch from "./ToggleSwitch";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+interface IProps {
+ // The value for the toggle switch
+ value: boolean;
+ // The translated label for the switch
+ label: string;
+ // Whether or not to disable the toggle switch
+ disabled?: boolean;
+ // True to put the toggle in front of the label
+ // Default false.
+ toggleInFront?: boolean;
+ // Additional class names to append to the switch. Optional.
+ className?: string;
+ // The function to call when the value changes
+ onChange(checked: boolean): void;
+}
+
@replaceableComponent("views.elements.LabelledToggleSwitch")
-export default class LabelledToggleSwitch extends React.Component {
- static propTypes = {
- // The value for the toggle switch
- value: PropTypes.bool.isRequired,
-
- // The function to call when the value changes
- onChange: PropTypes.func.isRequired,
-
- // The translated label for the switch
- label: PropTypes.string.isRequired,
-
- // Whether or not to disable the toggle switch
- disabled: PropTypes.bool,
-
- // True to put the toggle in front of the label
- // Default false.
- toggleInFront: PropTypes.bool,
-
- // Additional class names to append to the switch. Optional.
- className: PropTypes.string,
- };
-
+export default class LabelledToggleSwitch extends React.PureComponent {
render() {
// This is a minimal version of a SettingsFlag
- let firstPart = {this.props.label} ;
+ let firstPart = { this.props.label } ;
let secondPart = {
+ private fieldRef = createRef();
- constructor(props) {
- super(props);
- this.state = {isValid: true};
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isValid: true,
+ };
}
- _asFullAlias(localpart) {
+ private asFullAlias(localpart: string): string {
return `#${localpart}:${this.props.domain}`;
}
render() {
- const Field = sdk.getComponent('views.elements.Field');
const poundSign = (# );
const aliasPostfix = ":" + this.props.domain;
const domain = ({aliasPostfix} );
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
this._fieldRef = ref}
- onValidate={this._onValidate}
- placeholder={_t("e.g. my-room")}
- onChange={this._onChange}
+ ref={this.fieldRef}
+ onValidate={this.onValidate}
+ placeholder={this.props.placeholder || _t("e.g. my-room")}
+ onChange={this.onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength}
/>
);
}
- _onChange = (ev) => {
+ private onChange = (ev) => {
if (this.props.onChange) {
- this.props.onChange(this._asFullAlias(ev.target.value));
+ this.props.onChange(this.asFullAlias(ev.target.value));
}
};
- _onValidate = async (fieldState) => {
- const result = await this._validationRules(fieldState);
+ private onValidate = async (fieldState) => {
+ const result = await this.validationRules(fieldState);
this.setState({isValid: result.valid});
return result;
};
- _validationRules = withValidation({
+ private validationRules = withValidation({
rules: [
{
key: "safeLocalpart",
@@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
if (!value) {
return true;
}
- const fullAlias = this._asFullAlias(value);
+ const fullAlias = this.asFullAlias(value);
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
@@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
- invalid: () => _t("Please provide a room address"),
+ invalid: () => _t("Please provide an address"),
}, {
key: "taken",
final: true,
@@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
}
const client = MatrixClientPeg.get();
try {
- await client.getRoomIdForAlias(this._asFullAlias(value));
+ await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
@@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
],
});
- get isValid() {
+ public get isValid() {
return this.state.isValid;
}
- validate(options) {
- return this._fieldRef.validate(options);
+ public validate(options: IValidateOpts) {
+ return this.fieldRef.current?.validate(options);
}
- focus() {
- this._fieldRef.focus();
+ public focus() {
+ this.fieldRef.current?.focus();
}
}
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 4f885ab47d..24a21e1a33 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component {
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
- let label = this.props.label;
- if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
- else label = _t(label);
+ const label = this.props.label
+ ? _t(this.props.label)
+ : SettingsStore.getDisplayName(this.props.name, this.props.level);
+ const description = SettingsStore.getDescription(this.props.name);
if (this.props.useCheckbox) {
return {
disabled={this.props.disabled || !canChange}
aria-label={label}
/>
+ { description &&
+ { description }
+
}
);
}
diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx
index 6b9e992f92..744b6f2059 100644
--- a/src/components/views/elements/StyledRadioGroup.tsx
+++ b/src/components/views/elements/StyledRadioGroup.tsx
@@ -34,10 +34,19 @@ interface IProps {
definitions: IDefinition[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
+ disabled?: boolean;
onChange(newValue: T): void;
}
-function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) {
+function StyledRadioGroup({
+ name,
+ definitions,
+ value,
+ className,
+ outlined,
+ disabled,
+ onChange,
+}: IProps) {
const _onChange = e => {
onChange(e.target.value);
};
@@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
- disabled={d.disabled}
+ disabled={disabled || d.disabled}
outlined={outlined}
>
- {d.label}
+ { d.label }
- {d.description}
+ { d.description ? { d.description } : null }
)}
;
}
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.tsx
similarity index 65%
rename from src/components/views/elements/TruncatedList.js
rename to src/components/views/elements/TruncatedList.tsx
index 0509775545..395caa9222 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.tsx
@@ -16,31 +16,29 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
-@replaceableComponent("views.elements.TruncatedList")
-export default class TruncatedList extends React.Component {
- static propTypes = {
- // The number of elements to show before truncating. If negative, no truncation is done.
- truncateAt: PropTypes.number,
- // The className to apply to the wrapping div
- className: PropTypes.string,
- // A function that returns the children to be rendered into the element.
- // function getChildren(start: number, end: number): Array
- // The start element is included, the end is not (as in `slice`).
- // If omitted, the React child elements will be used. This parameter can be used
- // to avoid creating unnecessary React elements.
- getChildren: PropTypes.func,
- // A function that should return the total number of child element available.
- // Required if getChildren is supplied.
- getChildCount: PropTypes.func,
- // A function which will be invoked when an overflow element is required.
- // This will be inserted after the children.
- createOverflowElement: PropTypes.func,
- };
+interface IProps {
+ // The number of elements to show before truncating. If negative, no truncation is done.
+ truncateAt?: number;
+ // The className to apply to the wrapping div
+ className?: string;
+ // A function that returns the children to be rendered into the element.
+ // The start element is included, the end is not (as in `slice`).
+ // If omitted, the React child elements will be used. This parameter can be used
+ // to avoid creating unnecessary React elements.
+ getChildren?: (start: number, end: number) => Array;
+ // A function that should return the total number of child element available.
+ // Required if getChildren is supplied.
+ getChildCount?: () => number;
+ // A function which will be invoked when an overflow element is required.
+ // This will be inserted after the children.
+ createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
+}
+@replaceableComponent("views.elements.TruncatedList")
+export default class TruncatedList extends React.Component {
static defaultProps ={
truncateAt: 2,
createOverflowElement(overflowCount, totalCount) {
@@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
},
};
- _getChildren(start, end) {
+ private getChildren(start: number, end: number): Array {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildren(start, end);
} else {
@@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
}
}
- _getChildCount() {
+ private getChildCount(): number {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildCount();
} else {
@@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
}
}
- render() {
+ public render() {
let overflowNode = null;
- const totalChildren = this._getChildCount();
+ const totalChildren = this.getChildCount();
let upperBound = totalChildren;
if (this.props.truncateAt >= 0) {
const overflowCount = totalChildren - this.props.truncateAt;
@@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
upperBound = this.props.truncateAt;
}
}
- const childNodes = this._getChildren(0, upperBound);
+ const childNodes = this.getChildren(0, upperBound);
return (
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index c06d827550..6bef141cb8 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
return
-
+
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index 42a977fb79..dd8366bbe0 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import { Draggable, Droppable } from 'react-beautiful-dnd';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
-
-function nop() {}
+import { _t } from "../../../languageHandler";
+import TagOrderActions from "../../../actions/TagOrderActions";
+import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.groups.GroupTile")
class GroupTile extends React.Component {
@@ -34,7 +34,6 @@ class GroupTile extends React.Component {
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
- draggable: PropTypes.bool,
};
static contextType = MatrixClientContext;
@@ -42,7 +41,6 @@ class GroupTile extends React.Component {
static defaultProps = {
showDescription: true,
avatarHeight: 50,
- draggable: true,
};
state = {
@@ -57,7 +55,7 @@ class GroupTile extends React.Component {
});
}
- onMouseDown = e => {
+ onClick = e => {
e.preventDefault();
dis.dispatch({
action: 'view_group',
@@ -65,6 +63,18 @@ class GroupTile extends React.Component {
});
};
+ onPinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
+ };
+
+ onUnpinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
+ };
+
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@@ -78,7 +88,7 @@ class GroupTile extends React.Component {
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
: null;
- let avatarElement = (
+ const avatarElement = (
);
- if (this.props.draggable) {
- const avatarClone = avatarElement;
- avatarElement = (
-
- { (droppableProvided, droppableSnapshot) => (
-
-
- { (provided, snapshot) => (
-
-
- {avatarClone}
-
- { /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
- { provided.placeholder ? avatarClone :
}
-
- ) }
-
-
- ) }
-
- );
- }
- // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
- // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
- return
+ return
{ avatarElement }
{ name }
{ descElement }
{ this.props.groupId }
+ { !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
+ ?
+ { _t("Pin") }
+
+ :
+ { _t("Unpin") }
+
+ }
;
}
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.tsx
similarity index 76%
rename from src/components/views/messages/MKeyVerificationRequest.js
rename to src/components/views/messages/MKeyVerificationRequest.tsx
index 988606a766..69467cfa50 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.tsx
@@ -15,41 +15,40 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixEvent } from 'matrix-js-sdk/src';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
-import {getNameForEventRoom, userLabelForEventRoom}
+import { getNameForEventRoom, userLabelForEventRoom }
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher/dispatcher";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {Action} from "../../../dispatcher/actions";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { Action } from "../../../dispatcher/actions";
import EventTileBubble from "./EventTileBubble";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ mxEvent: MatrixEvent
+}
@replaceableComponent("views.messages.MKeyVerificationRequest")
-export default class MKeyVerificationRequest extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- componentDidMount() {
+export default class MKeyVerificationRequest extends React.Component {
+ public componentDidMount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
- request.on("change", this._onRequestChanged);
+ request.on("change", this.onRequestChanged);
}
}
- componentWillUnmount() {
+ public componentWillUnmount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
- request.off("change", this._onRequestChanged);
+ request.off("change", this.onRequestChanged);
}
}
- _openRequest = () => {
- const {verificationRequest} = this.props.mxEvent;
+ private openRequest = () => {
+ const { verificationRequest } = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({
action: Action.SetRightPanelPhase,
@@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component {
});
};
- _onRequestChanged = () => {
+ private onRequestChanged = () => {
this.forceUpdate();
};
- _onAcceptClicked = async () => {
+ private onAcceptClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
- this._openRequest();
+ this.openRequest();
await request.accept();
} catch (err) {
console.error(err.message);
@@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
- _onRejectClicked = async () => {
+ private onRejectClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
- _acceptedLabel(userId) {
+ private acceptedLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
@@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
}
- _cancelledLabel(userId) {
+ private cancelledLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const {cancellationCode} = this.props.mxEvent.verificationRequest;
@@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component {
}
}
- render() {
+ public render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
- const FormButton = sdk.getComponent("elements.FormButton");
const {mxEvent} = this.props;
const request = mxEvent.verificationRequest;
@@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component {
let stateLabel;
const accepted = request.ready || request.started || request.done;
if (accepted) {
- stateLabel = (
- {this._acceptedLabel(request.receivingUserId)}
+ stateLabel = (
+ {this.acceptedLabel(request.receivingUserId)}
);
} else if (request.cancelled) {
- stateLabel = this._cancelledLabel(request.cancellingUserId);
+ stateLabel = this.cancelledLabel(request.cancellingUserId);
} else if (request.accepting) {
stateLabel = _t("Accepting …");
} else if (request.declining) {
@@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component {
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
if (request.canAccept) {
stateNode = (
-
-
+
+ {_t("Decline")}
+
+
+ {_t("Accept")}
+
);
}
} else { // request sent by us
@@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component {
return null;
}
}
-
-MKeyVerificationRequest.propTypes = {
- /* the MatrixEvent to show */
- mxEvent: PropTypes.object.isRequired,
-};
diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx
index 0cebcf3440..6d26ef3dcb 100644
--- a/src/components/views/messages/MVoiceOrAudioBody.tsx
+++ b/src/components/views/messages/MVoiceOrAudioBody.tsx
@@ -28,7 +28,9 @@ interface IProps {
@replaceableComponent("views.messages.MVoiceOrAudioBody")
export default class MVoiceOrAudioBody extends React.PureComponent {
public render() {
- const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
+ // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
+ const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
+ || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
if (isVoiceMessage && voiceMessagesEnabled) {
return ;
diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index a72731522f..1131c02dbf 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, {useCallback, useContext, useEffect, useState} from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event';
@@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
import PinningUtils from "../../../utils/PinningUtils";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import PinnedEventTile from "../rooms/PinnedEventTile";
+import { useRoomState } from "../../../hooks/useRoomState";
interface IProps {
room: Room;
@@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set => {
return readPinnedEvents;
};
-const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => {
- const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
-
- const update = useCallback(() => {
- if (!room) return;
- setValue(mapper(room.currentState));
- }, [room, mapper]);
-
- useEventEmitter(room?.currentState, "RoomState.events", update);
- useEffect(() => {
- update();
- return () => {
- setValue(undefined);
- };
- }, [update]);
- return value;
-};
-
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
const cli = useContext(MatrixClientContext);
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 8833cb6862..e22b2d5ff4 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
import { Action } from "../../../dispatcher/actions";
-import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
+import { UserTab } from "../dialogs/UserSettingsDialog";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import { E2EStatus } from "../../../utils/ShieldUtils";
@@ -503,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
return member.powerLevel < levelToSend;
};
+const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
+
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
- const [powerLevels, setPowerLevels] = useState({});
+ const [powerLevels, setPowerLevels] = useState(getPowerLevels(room));
const update = useCallback((ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
-
- const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- if (event) {
- setPowerLevels(event.getContent());
- } else {
- setPowerLevels({});
- }
+ setPowerLevels(getPowerLevels(room));
}, [room]);
useEventEmitter(cli, "RoomState.events", update);
@@ -1381,7 +1377,7 @@ const BasicUserInfo: React.FC<{
{
dis.dispatch({
action: Action.ViewUserSettings,
- initialTabId: USER_SECURITY_TAB,
+ initialTabId: UserTab.Security,
});
}}>
{ _t("Edit devices") }
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index edfe0e3483..d3f2ba8cbf 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -195,14 +195,7 @@ export default class VerificationPanel extends React.PureComponent
{description}
-
{_t("No")}
-
+ { _t("No") }
+
+ {_t("Yes")}
+ onClick={this.onReciprocateYesClick}
+ >
+ { _t("Yes") }
+
;
} else {
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.tsx
similarity index 71%
rename from src/components/views/room_settings/AliasSettings.js
rename to src/components/views/room_settings/AliasSettings.tsx
index 80e0099ab3..59c4bf2c0c 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.tsx
@@ -1,6 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018, 2019 New Vector Ltd
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,59 +14,60 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React, { ChangeEvent, createRef } from "react";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
import EditableItemList from "../elements/EditableItemList";
-import React, {createRef} from 'react';
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import * as sdk from "../../../index";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
+import Spinner from "../elements/Spinner";
import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import RoomPublishSetting from "./RoomPublishSetting";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import RoomAliasField from "../elements/RoomAliasField";
-class EditableAliasesList extends EditableItemList {
- constructor(props) {
- super(props);
+interface IEditableAliasesListProps {
+ domain?: string;
+}
- this._aliasField = createRef();
- }
+class EditableAliasesList extends EditableItemList {
+ private aliasField = createRef();
- _onAliasAdded = async () => {
- await this._aliasField.current.validate({ allowEmpty: false });
+ private onAliasAdded = async () => {
+ await this.aliasField.current.validate({ allowEmpty: false });
- if (this._aliasField.current.isValid) {
+ if (this.aliasField.current.isValid) {
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
return;
}
- this._aliasField.current.focus();
- this._aliasField.current.validate({ allowEmpty: false, focused: true });
+ this.aliasField.current.focus();
+ this.aliasField.current.validate({ allowEmpty: false, focused: true });
};
- _renderNewItemField() {
+ protected renderNewItemField() {
// if we don't need the RoomAliasField,
- // we don't need to overriden version of _renderNewItemField
+ // we don't need to overriden version of renderNewItemField
if (!this.props.domain) {
- return super._renderNewItemField();
+ return super.renderNewItemField();
}
- const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
- const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
+ const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
return (
@@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
}
}
-@replaceableComponent("views.room_settings.AliasSettings")
-export default class AliasSettings extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- canSetCanonicalAlias: PropTypes.bool.isRequired,
- canSetAliases: PropTypes.bool.isRequired,
- canonicalAliasEvent: PropTypes.object, // MatrixEvent
- };
+interface IProps {
+ roomId: string;
+ canSetCanonicalAlias: boolean;
+ canSetAliases: boolean;
+ canonicalAliasEvent?: MatrixEvent;
+ hidePublishSetting?: boolean;
+}
+interface IState {
+ altAliases: string[];
+ localAliases: string[];
+ canonicalAlias?: string;
+ updatingCanonicalAlias: boolean;
+ localAliasesLoading: boolean;
+ detailsOpen: boolean;
+ newAlias?: string;
+ newAltAlias?: string;
+}
+
+@replaceableComponent("views.room_settings.AliasSettings")
+export default class AliasSettings extends React.Component {
static defaultProps = {
canSetAliases: false,
canSetCanonicalAlias: false,
- aliasEvents: [],
};
constructor(props) {
@@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
}
}
- async loadLocalAliases() {
+ private async loadLocalAliases() {
this.setState({ localAliasesLoading: true });
try {
const cli = MatrixClientPeg.get();
@@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component {
}
}
this.setState({ localAliases });
+
+ if (localAliases.length === 0) {
+ this.setState({ detailsOpen: true });
+ }
} finally {
this.setState({ localAliasesLoading: false });
}
}
- changeCanonicalAlias(alias) {
+ private changeCanonicalAlias(alias: string) {
if (!this.props.canSetCanonicalAlias) return;
const oldAlias = this.state.canonicalAlias;
@@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component {
});
}
- changeAltAliases(altAliases) {
+ private changeAltAliases(altAliases: string[]) {
if (!this.props.canSetCanonicalAlias) return;
this.setState({
@@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component {
const eventContent = {};
if (this.state.canonicalAlias) {
- eventContent.alias = this.state.canonicalAlias;
+ eventContent["alias"] = this.state.canonicalAlias;
}
if (altAliases) {
eventContent["alt_aliases"] = altAliases;
@@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component {
});
}
- onNewAliasChanged = (value) => {
- this.setState({newAlias: value});
+ private onNewAliasChanged = (value: string) => {
+ this.setState({ newAlias: value });
};
- onLocalAliasAdded = (alias) => {
+ private onLocalAliasAdded = (alias: string) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
const localDomain = MatrixClientPeg.get().getDomain();
@@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasDeleted = (index) => {
+ private onLocalAliasDeleted = (index: number) => {
const alias = this.state.localAliases[index];
// TODO: In future, we should probably be making sure that the alias actually belongs
// to this room. See https://github.com/vector-im/element-web/issues/7353
@@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasesToggled = (event) => {
+ private onLocalAliasesToggled = (event: ChangeEvent) => {
// expanded
if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount
@@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component {
this.loadLocalAliases();
}
}
- this.setState({detailsOpen: event.target.open});
+ this.setState({ detailsOpen: event.currentTarget.open });
};
- onCanonicalAliasChange = (event) => {
+ private onCanonicalAliasChange = (event: ChangeEvent) => {
this.changeCanonicalAlias(event.target.value);
};
- onNewAltAliasChanged = (value) => {
- this.setState({newAltAlias: value});
+ private onNewAltAliasChanged = (value: string) => {
+ this.setState({ newAltAlias: value });
}
- onAltAliasAdded = (alias) => {
+ private onAltAliasAdded = (alias: string) => {
const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim());
this.changeAltAliases(altAliases);
- this.setState({newAltAlias: ""});
+ this.setState({ newAltAlias: "" });
}
}
- onAltAliasDeleted = (index) => {
+ private onAltAliasDeleted = (index: number) => {
const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1);
this.changeAltAliases(altAliases);
}
- _getAliases() {
- return this.state.altAliases.concat(this._getLocalNonAltAliases());
+ private getAliases() {
+ return this.state.altAliases.concat(this.getLocalNonAltAliases());
}
- _getLocalNonAltAliases() {
+ private getLocalNonAltAliases() {
const {altAliases} = this.state;
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
}
render() {
- const localDomain = MatrixClientPeg.get().getDomain();
+ const cli = MatrixClientPeg.get();
+ const localDomain = cli.getDomain();
+ const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
@@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component {
>
{ _t('not specified') }
{
- this._getAliases().map((alias, i) => {
+ this.getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true;
return (
@@ -340,12 +357,10 @@ export default class AliasSettings extends React.Component {
let localAliasesList;
if (this.state.localAliasesLoading) {
- const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = ;
} else {
localAliasesList = ( );
@@ -362,18 +379,27 @@ export default class AliasSettings extends React.Component {
return (
{_t("Published Addresses")}
-
{_t("Published addresses can be used by anyone on any server to join your room. " +
- "To publish an address, it needs to be set as a local address first.")}
- {canonicalAliasSection}
-
+
+ { isSpaceRoom
+ ? _t("Published addresses can be used by anyone on any server to join your space.")
+ : _t("Published addresses can be used by anyone on any server to join your room.")}
+
+ { _t("To publish an address, it needs to be set as a local address first.") }
+
+ { canonicalAliasSection }
+ { this.props.hidePublishSetting
+ ? null
+ :
}
- {this._getLocalNonAltAliases().map(alias => {
+ {this.getLocalNonAltAliases().map(alias => {
return ;
})};
-
{_t("Local Addresses")}
-
{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}
-
+
+ { _t("Local Addresses") }
+
+
+ { isSpaceRoom
+ ? _t("Set addresses for this space so users can find this space " +
+ "through your homeserver (%(localDomain)s)", { localDomain })
+ : _t("Set addresses for this room so users can find this room " +
+ "through your homeserver (%(localDomain)s)", { localDomain }) }
+
+
{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}
- {localAliasesList}
+ { localAliasesList }
);
diff --git a/src/components/views/room_settings/RoomPublishSetting.js b/src/components/views/room_settings/RoomPublishSetting.tsx
similarity index 60%
rename from src/components/views/room_settings/RoomPublishSetting.js
rename to src/components/views/room_settings/RoomPublishSetting.tsx
index 6cc3ce26ba..95b0ac100d 100644
--- a/src/components/views/room_settings/RoomPublishSetting.js
+++ b/src/components/views/room_settings/RoomPublishSetting.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,20 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React from "react";
+
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import {_t} from "../../../languageHandler";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { _t } from "../../../languageHandler";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ label?: string;
+ canSetCanonicalAlias?: boolean;
+}
+
+interface IState {
+ isRoomPublished: boolean;
+}
@replaceableComponent("views.room_settings.RoomPublishSetting")
-export default class RoomPublishSetting extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {isRoomPublished: false};
+export default class RoomPublishSetting extends React.PureComponent {
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isRoomPublished: false,
+ };
}
- onRoomPublishChange = (e) => {
+ private onRoomPublishChange = (e) => {
const valueBefore = this.state.isRoomPublished;
const newValue = !valueBefore;
this.setState({isRoomPublished: newValue});
@@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
render() {
const client = MatrixClientPeg.get();
- return ( );
+ return (
+
+ );
}
}
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index f6cc0f4d45..04c7a57048 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -25,7 +25,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { UIFeature } from "../../../settings/UIFeature";
-import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
import CallViewForRoom from '../voip/CallViewForRoom';
import { objectHasDiff } from "../../../utils/objects";
import { replaceableComponent } from "../../../utils/replaceableComponent";
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 0099bf73fb..3d674efe04 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
-
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
@@ -29,24 +28,24 @@ import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {formatTime} from "../../../DateUtils";
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
+import { Layout } from "../../../settings/Layout";
+import { formatTime } from "../../../DateUtils";
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {E2E_STATE} from "./E2EIcon";
-import {toRem} from "../../../utils/units";
-import {WidgetType} from "../../../widgets/WidgetType";
+import { E2E_STATE } from "./E2EIcon";
+import { toRem } from "../../../utils/units";
+import { WidgetType } from "../../../widgets/WidgetType";
import RoomAvatar from "../avatars/RoomAvatar";
-import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
-import {objectHasDiff} from "../../../utils/objects";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { WIDGET_LAYOUT_EVENT_TYPE } from "../../../stores/widgets/WidgetLayoutStore";
+import { objectHasDiff } from "../../../utils/objects";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
-import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
+import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
-import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
+import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge";
-import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
+import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from '../../../dispatcher/actions';
const eventTileTypes = {
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 5a1c3a24b3..0b79f7b52e 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
-import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
@@ -466,6 +466,7 @@ export default class RoomList extends React.PureComponent {
}
private renderCommunityInvites(): ReactComponentElement[] {
+ if (SettingsStore.getValue("feature_spaces")) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index ba8bbffbcc..61166b4230 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -45,7 +45,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill";
-import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
deleted file mode 100644
index 029516c932..0000000000
--- a/src/components/views/rooms/SearchBar.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React, {createRef} from 'react';
-import AccessibleButton from "../elements/AccessibleButton";
-import classNames from "classnames";
-import { _t } from '../../../languageHandler';
-import {Key} from "../../../Keyboard";
-import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.rooms.SearchBar")
-export default class SearchBar extends React.Component {
- constructor(props) {
- super(props);
-
- this._search_term = createRef();
-
- this.state = {
- scope: 'Room',
- };
- }
-
- onThisRoomClick = () => {
- this.setState({ scope: 'Room' }, () => this._searchIfQuery());
- };
-
- onAllRoomsClick = () => {
- this.setState({ scope: 'All' }, () => this._searchIfQuery());
- };
-
- onSearchChange = (e) => {
- switch (e.key) {
- case Key.ENTER:
- this.onSearch();
- break;
- case Key.ESCAPE:
- this.props.onCancelClick();
- break;
- }
- };
-
- _searchIfQuery() {
- if (this._search_term.current.value) {
- this.onSearch();
- }
- }
-
- onSearch = () => {
- this.props.onSearch(this._search_term.current.value, this.state.scope);
- };
-
- render() {
- const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
- mx_SearchBar_searching: this.props.searchInProgress,
- });
- const thisRoomClasses = classNames("mx_SearchBar_button", {
- mx_SearchBar_unselected: this.state.scope !== 'Room',
- });
- const allRoomsClasses = classNames("mx_SearchBar_button", {
- mx_SearchBar_unselected: this.state.scope !== 'All',
- });
-
- return (
- <>
-
-
-
- {_t("This Room")}
-
-
- {_t("All Rooms")}
-
-
-
-
-
-
- >
- );
- }
-}
diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx
new file mode 100644
index 0000000000..d71bb8da73
--- /dev/null
+++ b/src/components/views/rooms/SearchBar.tsx
@@ -0,0 +1,130 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { createRef, RefObject } from 'react';
+import AccessibleButton from "../elements/AccessibleButton";
+import classNames from "classnames";
+import { _t } from '../../../languageHandler';
+import { Key } from "../../../Keyboard";
+import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ onCancelClick: () => void;
+ onSearch: (query: string, scope: string) => void;
+ searchInProgress?: boolean;
+ isRoomEncrypted?: boolean;
+}
+
+interface IState {
+ scope: SearchScope;
+}
+
+export enum SearchScope {
+ Room = "Room",
+ All = "All",
+}
+
+@replaceableComponent("views.rooms.SearchBar")
+export default class SearchBar extends React.Component {
+ private searchTerm: RefObject = createRef();
+
+ constructor(props: IProps) {
+ super(props);
+ this.state = {
+ scope: SearchScope.Room,
+ };
+ }
+
+ private onThisRoomClick = () => {
+ this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery());
+ };
+
+ private onAllRoomsClick = () => {
+ this.setState({ scope: SearchScope.All }, () => this.searchIfQuery());
+ };
+
+ private onSearchChange = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case Key.ENTER:
+ this.onSearch();
+ break;
+ case Key.ESCAPE:
+ this.props.onCancelClick();
+ break;
+ }
+ };
+
+ private searchIfQuery(): void {
+ if (this.searchTerm.current.value) {
+ this.onSearch();
+ }
+ }
+
+ private onSearch = (): void => {
+ this.props.onSearch(this.searchTerm.current.value, this.state.scope);
+ };
+
+ public render() {
+ const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
+ mx_SearchBar_searching: this.props.searchInProgress,
+ });
+ const thisRoomClasses = classNames("mx_SearchBar_button", {
+ mx_SearchBar_unselected: this.state.scope !== SearchScope.Room,
+ });
+ const allRoomsClasses = classNames("mx_SearchBar_button", {
+ mx_SearchBar_unselected: this.state.scope !== SearchScope.All,
+ });
+
+ return (
+ <>
+
+
+
+ {_t("This Room")}
+
+
+ {_t("All Rooms")}
+
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index 2102071bf3..122ba0ca0b 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback";
import {MsgType} from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
-import CallMediaHandler from "../../../CallMediaHandler";
+import MediaDeviceHandler from "../../../MediaDeviceHandler";
interface IProps {
room: Room;
@@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)),
},
- "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint
+ "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
await this.disposeRecording();
}
@@ -132,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
similarity index 61%
rename from src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index 28aad65129..c4963d0154 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,68 +15,76 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from "../../../../../languageHandler";
-import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
-import * as sdk from "../../../../..";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+
+import { _t } from "../../../../../languageHandler";
+import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
+import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
+import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
-import {replaceableComponent} from "../../../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ closeSettingsFn(): void;
+}
+
+interface IRecommendedVersion {
+ version: string;
+ needsUpgrade: boolean;
+ urgent: boolean;
+}
+
+interface IState {
+ upgradeRecommendation?: IRecommendedVersion;
+ oldRoomId?: string;
+ oldEventId?: string;
+ upgraded?: boolean;
+}
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
-export default class AdvancedRoomSettingsTab extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- closeSettingsFn: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
+export default class AdvancedRoomSettingsTab extends React.Component {
+ constructor(props, context) {
+ super(props, context);
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
};
- }
- // TODO: [REACT-WARNING] Move this to constructor
- UNSAFE_componentWillMount() { // eslint-disable-line camelcase
// we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room.getRecommendedVersion().then((v) => {
- const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
+ const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
- const additionalStateChanges = {};
- const createEvent = room.currentState.getStateEvents("m.room.create", "");
+ const additionalStateChanges: Partial = {};
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
if (predecessor && predecessor.room_id) {
- additionalStateChanges['oldRoomId'] = predecessor.room_id;
- additionalStateChanges['oldEventId'] = predecessor.event_id;
- additionalStateChanges['hasPreviousRoom'] = true;
+ additionalStateChanges.oldRoomId = predecessor.room_id;
+ additionalStateChanges.oldEventId = predecessor.event_id;
}
-
this.setState({
- upgraded: tombstone && tombstone.getContent().replacement_room,
+ upgraded: !!tombstone?.getContent().replacement_room,
upgradeRecommendation: v,
...additionalStateChanges,
});
});
}
- _upgradeRoom = (e) => {
- const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
+ private upgradeRoom = (e) => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
- Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
+ Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
};
- _openDevtools = (e) => {
- const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
+ private openDevtools = (e) => {
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
};
- _onOldRoomClicked = (e) => {
+ private onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
@@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
const room = client.getRoom(this.props.roomId);
let unfederatableSection;
- const createEvent = room.currentState.getStateEvents('m.room.create', '');
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
- unfederatableSection = {_t('This room is not accessible by remote Matrix servers')}
;
+ unfederatableSection = { _t('This room is not accessible by remote Matrix servers') }
;
}
let roomUpgradeButton;
@@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
roomUpgradeButton = (
- {_t(
+ { _t(
"Warning : Upgrading a room will not automatically migrate room members " +
"to the new version of the room. We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
@@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
"b": (sub) => {sub} ,
"i": (sub) => {sub} ,
},
- )}
+ ) }
-
- {_t("Upgrade this room to the recommended room version")}
+
+ { _t("Upgrade this room to the recommended room version") }
);
}
let oldRoomLink;
- if (this.state.hasPreviousRoom) {
+ if (this.state.oldRoomId) {
let name = _t("this room");
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room && room.name) name = room.name;
oldRoomLink = (
-
- {_t("View older messages in %(roomName)s.", {roomName: name})}
+
+ { _t("View older messages in %(roomName)s.", { roomName: name }) }
);
}
return (
-
{_t("Advanced")}
+
{ _t("Advanced") }
-
{_t("Room information")}
+
+ { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
+
- {_t("Internal room ID:")}
- {this.props.roomId}
+ { _t("Internal room ID:") }
+ { this.props.roomId }
- {unfederatableSection}
+ { unfederatableSection }
-
{_t("Room version")}
+
{ _t("Room version") }
- {_t("Room version:")}
- {room.getVersion()}
+ { _t("Room version:") }
+ { room.getVersion() }
- {oldRoomLink}
- {roomUpgradeButton}
+ { oldRoomLink }
+ { roomUpgradeButton }
-
{_t("Developer options")}
-
- {_t("Open Devtools")}
+ { _t("Developer options") }
+
+ { _t("Open Devtools") }
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 139cfd5fbd..10c93c5dca 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
- const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
@@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
+ canonicalAliasEvent={canonicalAliasEv} />
{_t("Other")}
{ flairSection }
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 02bbcfb751..bb7e194253 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
// Knock and private are reserved keywords which are not yet implemented.
-enum JoinRule {
+export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
Private = "private",
}
-enum GuestAccess {
+export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
-enum HistoryVisibility {
+export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
+ private onEncryptionChange = () => {
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'),
description: _t(
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index 3fa0be478c..beff033001 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu";
import { toRightOf } from "../../../../structures/ContextMenu";
interface IProps {
- closeSettingsFn: () => {};
+ closeSettingsFn: () => void;
}
interface IState {
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index 362059f8ed..f730406eed 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
-import CallMediaHandler from "../../../../../CallMediaHandler";
+import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
@@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
}
async componentDidMount() {
- const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
+ const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this._refreshMediaDevices();
}
@@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
_refreshMediaDevices = async (stream) => {
this.setState({
- mediaDevices: await CallMediaHandler.getDevices(),
- activeAudioOutput: CallMediaHandler.getAudioOutput(),
- activeAudioInput: CallMediaHandler.getAudioInput(),
- activeVideoInput: CallMediaHandler.getVideoInput(),
+ mediaDevices: await MediaDeviceHandler.getDevices(),
+ activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
+ activeAudioInput: MediaDeviceHandler.getAudioInput(),
+ activeVideoInput: MediaDeviceHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
@@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
};
_setAudioOutput = (e) => {
- CallMediaHandler.setAudioOutput(e.target.value);
+ MediaDeviceHandler.instance.setAudioOutput(e.target.value);
this.setState({
activeAudioOutput: e.target.value,
});
};
_setAudioInput = (e) => {
- CallMediaHandler.setAudioInput(e.target.value);
+ MediaDeviceHandler.instance.setAudioInput(e.target.value);
this.setState({
activeAudioInput: e.target.value,
});
};
_setVideoInput = (e) => {
- CallMediaHandler.setVideoInput(e.target.value);
+ MediaDeviceHandler.instance.setVideoInput(e.target.value);
this.setState({
activeVideoInput: e.target.value,
});
@@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
}
};
- const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
+ const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = (
@@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
);
}
- const audioInputs = this.state.mediaDevices.audioinput.slice(0);
+ const audioInputs = this.state.mediaDevices.audioInput.slice(0);
if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = (
@@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
);
}
- const videoInputs = this.state.mediaDevices.videoinput.slice(0);
+ const videoInputs = this.state.mediaDevices.videoInput.slice(0);
if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = (
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 6a935ab276..2d096e1b9f 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -29,12 +29,13 @@ import AccessibleButton from "../elements/AccessibleButton";
import {BetaPill} from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
-import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
+import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
+import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
],
});
+const nameToAlias = (name: string, domain: string): string => {
+ const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
+ return `#${localpart}:${domain}`;
+};
+
const SpaceCreateMenu = ({ onFinished }) => {
const cli = useContext(MatrixClientContext);
const [visibility, setVisibility] = useState(null);
@@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
const [name, setName] = useState("");
const spaceNameField = useRef();
+ const [alias, setAlias] = useState("");
+ const spaceAliasField = useRef();
const [avatar, setAvatar] = useState(null);
const [topic, setTopic] = useState("");
@@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
setBusy(false);
return;
}
+ // validate the space name alias field but do not require it
+ if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
+ spaceAliasField.current.focus();
+ spaceAliasField.current.validate({ allowEmpty: true, focused: true });
+ setBusy(false);
+ return;
+ }
const initialState: ICreateRoomStateEvent[] = [
{
@@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
content: { url },
});
}
- if (topic) {
- initialState.push({
- type: EventType.RoomTopic,
- content: { topic },
- });
- }
try {
await createRoom({
@@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
creation_content: {
- // Based on MSC1840
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: initialState,
@@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
},
+ room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
+ topic,
},
spinner: false,
encryption: false,
@@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
;
} else {
+ const domain = cli.getDomain();
body =
{
label={_t("Name")}
autoFocus={true}
value={name}
- onChange={ev => setName(ev.target.value)}
+ onChange={ev => {
+ const newName = ev.target.value;
+ if (!alias || alias === nameToAlias(name, domain)) {
+ setAlias(nameToAlias(newName, domain));
+ }
+ setName(newName);
+ }}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/>
+ { visibility === Visibility.Public
+ ?
+ : null
+ }
+
{
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
- initialTabId: USER_LABS_TAB,
+ initialTabId: UserTab.Labs,
});
}} />
{ body }
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index eb63b21f0e..b33fcf8915 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -14,18 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useEffect, useState } from "react";
+import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
+import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import classNames from "classnames";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
-import {_t} from "../../../languageHandler";
+import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
-import {useContextMenu} from "../../structures/ContextMenu";
+import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
-import {SpaceItem} from "./SpaceTreeLevel";
+import { SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore, {
+ HOME_SPACE,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
@@ -37,9 +39,10 @@ import {
RovingAccessibleTooltipButton,
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
-import {Key} from "../../../Keyboard";
-import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
-import {NotificationState} from "../../../stores/notifications/NotificationState";
+import { Key } from "../../../Keyboard";
+import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
+import { NotificationState } from "../../../stores/notifications/NotificationState";
+import SettingsStore from "../../../settings/SettingsStore";
interface IButtonProps {
space?: Room;
@@ -120,11 +123,65 @@ const useSpaces = (): [Room[], Room[], Room | null] => {
return [invites, spaces, activeSpace];
};
+interface IInnerSpacePanelProps {
+ children?: ReactNode;
+ isPanelCollapsed: boolean;
+ setPanelCollapsed: Dispatch>;
+}
+
+// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
+const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => {
+ const [invites, spaces, activeSpace] = useSpaces();
+ const activeSpaces = activeSpace ? [activeSpace] : [];
+
+ const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
+ ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
+
+ return
+ SpaceStore.instance.setActiveSpace(null)}
+ selected={!activeSpace}
+ tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
+ notificationState={homeNotificationState}
+ isNarrow={isPanelCollapsed}
+ />
+ { invites.map(s => (
+ setPanelCollapsed(false)}
+ />
+ )) }
+ { spaces.map((s, i) => (
+
+ {(provided, snapshot) => (
+ setPanelCollapsed(false)}
+ />
+ )}
+
+ )) }
+ { children }
+
;
+});
+
const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
- const [invites, spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
useEffect(() => {
@@ -133,10 +190,6 @@ const SpacePanel = () => {
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
- const newClasses = classNames("mx_SpaceButton_new", {
- mx_SpaceButton_newCancel: menuDisplayed,
- });
-
let contextMenu = null;
if (menuDisplayed) {
contextMenu = ;
@@ -203,59 +256,61 @@ const SpacePanel = () => {
}
};
- const activeSpaces = activeSpace ? [activeSpace] : [];
- const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
- // TODO drag and drop for re-arranging order
- return
- {({onKeyDownHandler}) => (
-
-
-
-
SpaceStore.instance.setActiveSpace(null)}
- selected={!activeSpace}
- tooltip={_t("All rooms")}
- notificationState={RoomNotificationStateStore.instance.globalState}
- isNarrow={isPanelCollapsed}
+ const onNewClick = menuDisplayed ? closeMenu : () => {
+ if (!isPanelCollapsed) setPanelCollapsed(true);
+ openMenu();
+ };
+
+ return (
+ {
+ if (!result.destination) return; // dropped outside the list
+ SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
+ }}>
+
+ {({onKeyDownHandler}) => (
+
+
+ {(provided, snapshot) => (
+
+
+ { provided.placeholder }
+
+
+
+
+ )}
+
+ setPanelCollapsed(!isPanelCollapsed)}
+ title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
- { invites.map(s => setPanelCollapsed(false)}
- />) }
- { spaces.map(s => setPanelCollapsed(false)}
- />) }
-
- {
- if (!isPanelCollapsed) setPanelCollapsed(true);
- openMenu();
- }}
- isNarrow={isPanelCollapsed}
- />
-
- setPanelCollapsed(!isPanelCollapsed)}
- title={expandCollapseButtonTitle}
- />
- { contextMenu }
-
- )}
-
+ { contextMenu }
+
+ )}
+
+
+ );
};
export default SpacePanel;
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
new file mode 100644
index 0000000000..3afdc629e4
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -0,0 +1,143 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
+import SpaceBasicSettings from "./SpaceBasicSettings";
+import { avatarUrlForRoom } from "../../../Avatar";
+import { IDialogProps } from "../dialogs/IDialogProps";
+import { getTopic } from "../elements/RoomTopic";
+import { defaultDispatcher } from "../../../dispatcher/dispatcher";
+
+interface IProps extends IDialogProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
+ const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
+ const avatarChanged = newAvatar !== null;
+
+ const [name, setName] = useState(space.name);
+ const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
+ const nameChanged = name !== space.name;
+
+ const currentTopic = getTopic(space);
+ const [topic, setTopic] = useState(currentTopic);
+ const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
+ const topicChanged = topic !== currentTopic;
+
+ const onCancel = () => {
+ setNewAvatar(null);
+ setName(space.name);
+ setTopic(currentTopic);
+ };
+
+ const onSave = async () => {
+ setBusy(true);
+ const promises = [];
+
+ if (avatarChanged) {
+ if (newAvatar) {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
+ url: await cli.uploadContent(newAvatar),
+ }, ""));
+ } else {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
+ }
+ }
+
+ if (nameChanged) {
+ promises.push(cli.setRoomName(space.roomId, name));
+ }
+
+ if (topicChanged) {
+ promises.push(cli.setRoomTopic(space.roomId, topic));
+ }
+
+ const results = await Promise.allSettled(promises);
+ setBusy(false);
+ const failures = results.filter(r => r.status === "rejected");
+ if (failures.length > 0) {
+ console.error("Failed to save space settings: ", failures);
+ setError(_t("Failed to save space settings."));
+ }
+ };
+
+ return
+
{ _t("General") }
+
+
{ _t("Edit settings relating to your space.") }
+
+ { error &&
{ error }
}
+
+
onFinished(false)} />
+
+
+
+
+
+ { _t("Cancel") }
+
+
+ { busy ? _t("Saving...") : _t("Save Changes") }
+
+
+
+ {_t("Leave Space")}
+
+
{
+ defaultDispatcher.dispatch({
+ action: "leave_room",
+ room_id: space.roomId,
+ });
+ }}
+ >
+ { _t("Leave Space") }
+
+
+ ;
+};
+
+export default SpaceSettingsGeneralTab;
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
new file mode 100644
index 0000000000..263823603b
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -0,0 +1,187 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import AliasSettings from "../room_settings/AliasSettings";
+import { useStateToggle } from "../../../hooks/useStateToggle";
+import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
+import StyledRadioGroup from "../elements/StyledRadioGroup";
+
+interface IProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+enum SpaceVisibility {
+ Unlisted = "unlisted",
+ Private = "private",
+}
+
+const useLocalEcho = (
+ currentFactory: () => T,
+ setterFn: (value: T) => Promise,
+ errorFn: (error: Error) => void,
+): [value: T, handler: (value: T) => void] => {
+ const [value, setValue] = useState(currentFactory);
+ const handler = async (value: T) => {
+ setValue(value);
+ try {
+ await setterFn(value);
+ } catch (e) {
+ setValue(currentFactory());
+ errorFn(e);
+ }
+ };
+
+ return [value, handler];
+};
+
+const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [visibility, setVisibility] = useLocalEcho(
+ () => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
+ visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
+ join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
+ }, ""),
+ () => setError(_t("Failed to update the visibility of this space")),
+ );
+ const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
+ ?.getContent()?.guest_access === GuestAccess.CanJoin,
+ guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
+ guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
+ }, ""),
+ () => setError(_t("Failed to update the guest access of this space")),
+ );
+ const [historyVisibility, setHistoryVisibility] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
+ ?.getContent()?.history_visibility || HistoryVisibility.Shared,
+ historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
+ history_visibility: historyVisibility,
+ }, ""),
+ () => setError(_t("Failed to update the history visibility of this space")),
+ );
+
+ const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
+
+ const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
+ const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
+ const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
+ const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
+ const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
+
+ let advancedSection;
+ if (showAdvancedSection) {
+ advancedSection = <>
+
+ { _t("Hide advanced") }
+
+
+
+
+ { _t("Guests can join a space without having an account.") }
+
+ { _t("This may be useful for public spaces.") }
+
+ >;
+ } else {
+ advancedSection = <>
+
+ { _t("Show advanced") }
+
+ >;
+ }
+
+ let addressesSection;
+ if (visibility !== SpaceVisibility.Private) {
+ addressesSection = <>
+ {_t("Address")}
+
+ >;
+ }
+
+ return
+
{ _t("Visibility") }
+
+ { error &&
{ error }
}
+
+
+
+ { _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
+
+
+
+
+
+
+ { advancedSection }
+
+
{
+ setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
+ }}
+ disabled={!canSetHistoryVisibility}
+ label={_t("Preview Space")}
+ />
+ { _t("Allow people to preview your space before they join.") }
+ { _t("Recommended for public spaces.") }
+
+
+ { addressesSection }
+
;
+};
+
+export default SpaceSettingsVisibilityTab;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index f34baf256b..75ca641320 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
import classNames from "classnames";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
-import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
-import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
+import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
-import {_t} from "../../../languageHandler";
-import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
-import {toRightOf} from "../../structures/ContextMenu";
+import { _t } from "../../../languageHandler";
+import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
+import { toRightOf } from "../../structures/ContextMenu";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
@@ -39,33 +38,38 @@ import {
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {Action} from "../../../dispatcher/actions";
+import { Action } from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
-import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {EventType} from "matrix-js-sdk/src/@types/event";
-import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
-import {NotificationColor} from "../../../stores/notifications/NotificationColor";
+import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
+import { NotificationColor } from "../../../stores/notifications/NotificationColor";
+import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
-interface IItemProps {
+interface IItemProps extends InputHTMLAttributes {
space?: Room;
activeSpaces: Room[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
parents?: Set;
+ innerRef?: LegacyRef;
}
interface IItemState {
collapsed: boolean;
contextMenuPosition: Pick;
+ childSpaces: Room[];
}
export class SpaceItem extends React.PureComponent {
static contextType = MatrixClientContext;
+ private buttonRef = createRef();
+
constructor(props) {
super(props);
@@ -78,14 +82,36 @@ export class SpaceItem extends React.PureComponent {
this.state = {
collapsed: collapsed,
contextMenuPosition: null,
+ childSpaces: this.childSpaces,
};
+
+ SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
}
- private toggleCollapse(evt) {
- if (this.props.onExpand && this.state.collapsed) {
+ componentWillUnmount() {
+ SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
+ }
+
+ private onSpaceUpdate = () => {
+ this.setState({
+ childSpaces: this.childSpaces,
+ });
+ };
+
+ private get childSpaces() {
+ return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
+ .filter(s => !this.props.parents?.has(s.roomId));
+ }
+
+ private get isCollapsed() {
+ return this.state.collapsed || this.props.isPanelCollapsed;
+ }
+
+ private toggleCollapse = evt => {
+ if (this.props.onExpand && this.isCollapsed) {
this.props.onExpand();
}
- const newCollapsedState = !this.state.collapsed;
+ const newCollapsedState = !this.isCollapsed;
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
this.props.space.roomId,
@@ -96,7 +122,7 @@ export class SpaceItem extends React.PureComponent {
// don't bubble up so encapsulating button for space
// doesn't get triggered
evt.stopPropagation();
- }
+ };
private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return;
@@ -111,6 +137,43 @@ export class SpaceItem extends React.PureComponent {
});
}
+ private onKeyDown = (ev: React.KeyboardEvent) => {
+ let handled = true;
+ const action = getKeyBindingsManager().getRoomListAction(ev);
+ const hasChildren = this.state.childSpaces?.length;
+ switch (action) {
+ case RoomListAction.CollapseSection:
+ if (hasChildren && !this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
+ const parentButton = parentItem?.previousElementSibling as HTMLElement;
+ parentButton?.focus();
+ }
+ break;
+
+ case RoomListAction.ExpandSection:
+ if (hasChildren) {
+ if (this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const childLevel = this.buttonRef?.current?.nextElementSibling;
+ const firstSpaceItemChild = childLevel?.querySelector(".mx_SpaceItem");
+ firstSpaceItemChild?.querySelector(".mx_SpaceButton")?.focus();
+ }
+ }
+ break;
+
+ default:
+ handled = false;
+ }
+
+ if (handled) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ };
+
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -300,27 +363,25 @@ export class SpaceItem extends React.PureComponent {
}
render() {
- const {space, activeSpaces, isNested} = this.props;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
+ ...otherProps } = this.props;
- const forceCollapsed = this.props.isPanelCollapsed;
- const isNarrow = this.props.isPanelCollapsed;
- const collapsed = this.state.collapsed || forceCollapsed;
+ const collapsed = this.isCollapsed;
- const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
- .filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space);
- const itemClasses = classNames({
+ const itemClasses = classNames(this.props.className, {
"mx_SpaceItem": true,
- "mx_SpaceItem_narrow": isNarrow,
+ "mx_SpaceItem_narrow": isPanelCollapsed,
"collapsed": collapsed,
- "hasSubSpaces": childSpaces && childSpaces.length,
+ "hasSubSpaces": this.state.childSpaces?.length,
});
const isInvite = space.getMyMembership() === "invite";
const classes = classNames("mx_SpaceButton", {
mx_SpaceButton_active: isActive,
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
- mx_SpaceButton_narrow: isNarrow,
+ mx_SpaceButton_narrow: isPanelCollapsed,
mx_SpaceButton_invite: isInvite,
});
const notificationState = isInvite
@@ -328,12 +389,12 @@ export class SpaceItem extends React.PureComponent {
: SpaceStore.instance.getNotificationState(space.roomId);
let childItems;
- if (childSpaces && !collapsed) {
+ if (this.state.childSpaces?.length && !collapsed) {
childItems = ;
}
@@ -346,53 +407,36 @@ export class SpaceItem extends React.PureComponent {
const avatarSize = isNested ? 24 : 32;
- const toggleCollapseButton = childSpaces && childSpaces.length ?
+ const toggleCollapseButton = this.state.childSpaces?.length ?
this.toggleCollapse(evt)}
+ onClick={this.toggleCollapse}
+ tabIndex={-1}
+ aria-label={collapsed ? _t("Expand") : _t("Collapse")}
/> : null;
- let button;
- if (isNarrow) {
- button = (
+ return (
+
{ toggleCollapseButton }
+ { !isPanelCollapsed && { space.name } }
{ notifBadge }
{ this.renderContextMenu() }
- );
- } else {
- button = (
-
- { toggleCollapseButton }
-
-
- { space.name }
- { notifBadge }
- { this.renderContextMenu() }
-
-
- );
- }
- return (
-
- { button }
{ childItems }
);
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx
index 209babbf9d..45b65ae1fb 100644
--- a/src/components/views/toasts/GenericToast.tsx
+++ b/src/components/views/toasts/GenericToast.tsx
@@ -15,8 +15,8 @@ limitations under the License.
*/
import React, {ReactNode} from "react";
+import AccessibleButton from "../elements/AccessibleButton";
-import FormButton from "../elements/FormButton";
import {XOR} from "../../../@types/common";
export interface IProps {
@@ -50,8 +50,12 @@ const GenericToast: React.FC> = ({
{detailContent}
- {onReject && rejectLabel &&
}
-
+ {onReject && rejectLabel &&
+ { rejectLabel }
+ }
+
+ { acceptLabel }
+
;
};
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index c78f0c0fc8..d29caf789e 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React, {createRef} from 'react';
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger';
-import CallMediaHandler from "../../../CallMediaHandler";
+import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
interface IProps {
feed: CallFeed,
@@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component {
private element = createRef();
componentDidMount() {
+ MediaDeviceHandler.instance.addListener(
+ MediaDeviceHandlerEvent.AudioOutputChanged,
+ this.onAudioOutputChanged,
+ );
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.playMedia();
}
componentWillUnmount() {
+ MediaDeviceHandler.instance.removeListener(
+ MediaDeviceHandlerEvent.AudioOutputChanged,
+ this.onAudioOutputChanged,
+ );
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
this.stopMedia();
}
- private playMedia() {
+ private onAudioOutputChanged = (audioOutput: string) => {
const element = this.element.current;
- const audioOutput = CallMediaHandler.getAudioOutput();
-
if (audioOutput) {
try {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
@@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component {
logger.warn("Couldn't set requested audio output device: using default", e);
}
}
+ }
+ private playMedia() {
+ const element = this.element.current;
+ this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
element.muted = false;
element.srcObject = this.props.feed.stream;
element.autoplay = true;
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index a0660318bc..c09043da24 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads';
import CallHandler, { AudioID } from '../../../CallHandler';
import RoomAvatar from '../avatars/RoomAvatar';
-import FormButton from '../elements/FormButton';
+import AccessibleButton from '../elements/AccessibleButton';
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
@@ -143,21 +143,22 @@ export default class IncomingCallBox extends React.Component {
/>