{
+ private onDoneClick = () => {
+ this.props.onFinished(true);
+ };
+
+ private onGoToElementClick = () => {
+ this.props.onFinished(true);
+ };
+
+ private onRemindMeLaterClick = () => {
+ this.props.onFinished(false);
+ };
+
+ private getPrettyTargetUrl() {
+ const u = new URL(this.props.targetUrl);
+ let ret = u.host;
+ if (u.pathname !== '/') ret += u.pathname;
+ return ret;
+ }
+
+ getBodyText() {
+ if (this.props.kind === RebrandDialogKind.NAG) {
+ return _t(
+ "Use your account to sign in to the latest version of the app at ", {},
+ {
+ a: sub => {this.getPrettyTargetUrl()},
+ },
+ );
+ } else {
+ return _t(
+ "You’re already signed in and good to go here, but you can also grab the latest " +
+ "versions of the app on all platforms at element.io/get-started.", {},
+ {
+ a: sub => {sub},
+ },
+ );
+ }
+ }
+
+ getDialogButtons() {
+ if (this.props.kind === RebrandDialogKind.NAG) {
+ return ;
+ } else {
+ return ;
+ }
+ }
+
+ render() {
+ return
+ {this.getBodyText()}
+
+
})
+
+
})
+
+
+ {this.getDialogButtons()}
+ ;
+ }
+}
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index c2b98cd9f3..7ad1001f75 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
+export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
+export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
+export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
+export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
+export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
+export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
+
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
@@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
+ ROOM_GENERAL_TAB,
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
,
));
tabs.push(new Tab(
+ ROOM_SECURITY_TAB,
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
,
));
tabs.push(new Tab(
+ ROOM_ROLES_TAB,
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
,
));
tabs.push(new Tab(
+ ROOM_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_RoomSettingsDialog_notificationsIcon",
,
@@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component {
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
tabs.push(new Tab(
+ ROOM_BRIDGES_TAB,
_td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon",
,
@@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component {
}
tabs.push(new Tab(
+ ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
,
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
index 02534c5b35..c83528c5ba 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 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.
@@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
+import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@@ -63,6 +64,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
};
render() {
+ const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@@ -96,8 +98,11 @@ export default class RoomUpgradeWarningDialog extends React.Component {
{_t(
"This usually only affects how the room is processed on the server. If you're " +
- "having problems with your Riot, please report a bug.",
- {}, {
+ "having problems with your %(brand)s, please report a bug.",
+ {
+ brand,
+ },
+ {
"a": (sub) => {
return {sub};
},
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
new file mode 100644
index 0000000000..f6767dcb8d
--- /dev/null
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -0,0 +1,124 @@
+/*
+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 * as React from 'react';
+import BaseDialog from './BaseDialog';
+import { _t } from '../../../languageHandler';
+import { EchoStore } from "../../../stores/local-echo/EchoStore";
+import { formatTime } from "../../../DateUtils";
+import SettingsStore from "../../../settings/SettingsStore";
+import { RoomEchoContext } from "../../../stores/local-echo/RoomEchoContext";
+import RoomAvatar from "../avatars/RoomAvatar";
+import { TransactionStatus } from "../../../stores/local-echo/EchoTransaction";
+import Spinner from "../elements/Spinner";
+import AccessibleButton from "../elements/AccessibleButton";
+import { UPDATE_EVENT } from "../../../stores/AsyncStore";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+
+interface IProps {
+ onFinished: (bool) => void;
+}
+
+export default class ServerOfflineDialog extends React.PureComponent {
+ public componentDidMount() {
+ EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
+ }
+
+ public componentWillUnmount() {
+ EchoStore.instance.off(UPDATE_EVENT, this.onEchosUpdated);
+ }
+
+ private onEchosUpdated = () => {
+ this.forceUpdate(); // no state to worry about
+ };
+
+ private renderTimeline(): React.ReactElement[] {
+ return EchoStore.instance.contexts.map((c, i) => {
+ if (!c.firstFailedTime) return null; // not useful
+ if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c);
+ const header = (
+
+
+ {c.room.name}
+
+ );
+ const entries = c.transactions
+ .filter(t => t.status === TransactionStatus.Error || t.didPreviouslyFail)
+ .map((t, j) => {
+ let button = ;
+ if (t.status === TransactionStatus.Error) {
+ button = (
+ t.run()}>{_t("Resend")}
+ );
+ }
+ return (
+
+
+ {t.auditName}
+
+ {button}
+
+ );
+ });
+ return (
+
+
+ {formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
+
+
+ {header}
+ {entries}
+
+
+ )
+ });
+ }
+
+ public render() {
+ let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check
+ if (timeline.length === 0) {
+ timeline = [{_t("You're all caught up.")}
];
+ }
+
+ const serverName = MatrixClientPeg.getHomeserverName();
+ return
+
+
{_t(
+ "Your server isn't responding to some of your requests. " +
+ "Below are some of the most likely reasons.",
+ )}
+
+ - {_t("The server (%(serverName)s) took too long to respond.", {serverName})}
+ - {_t("Your firewall or anti-virus is blocking the request.")}
+ - {_t("A browser extension is preventing the request.")}
+ - {_t("The server is offline.")}
+ - {_t("The server has denied your request.")}
+ - {_t("Your area is experiencing difficulties connecting to the internet.")}
+ - {_t("A connection error occurred while trying to contact the server.")}
+ - {_t("The server is not configured to indicate what the problem is (CORS).")}
+
+
+
{_t("Recent changes that have not yet been received")}
+ {timeline}
+
+ ;
+ }
+}
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js
index 935faf0cad..3706172085 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.js
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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.
@@ -56,6 +57,7 @@ export default createReactClass({
},
render: function() {
+ const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@@ -94,9 +96,10 @@ export default createReactClass({
{ _t("We encountered an error trying to restore your previous session.") }
{ _t(
- "If you have previously used a more recent version of Riot, your session " +
+ "If you have previously used a more recent version of %(brand)s, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.",
+ { brand },
) }
{ _t(
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 4592d921a9..1f1a8d1523 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -33,9 +33,21 @@ import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
+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 default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
+ initialTabId: PropTypes.string,
};
constructor() {
@@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
+ USER_GENERAL_TAB,
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
,
));
tabs.push(new Tab(
+ USER_APPEARANCE_TAB,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
,
));
tabs.push(new Tab(
+ USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
,
));
tabs.push(new Tab(
+ USER_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
,
));
tabs.push(new Tab(
+ USER_PREFERENCES_TAB,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
,
));
tabs.push(new Tab(
+ USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
,
));
tabs.push(new Tab(
+ USER_SECURITY_TAB,
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
+ USER_LABS_TAB,
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
,
@@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component {
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
+ USER_MJOLNIR_TAB,
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
,
));
}
tabs.push(new Tab(
+ USER_HELP_TAB,
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
,
@@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
-
+
);
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
index 162cb4736a..42a5304f13 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -17,10 +17,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
-import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
+import SettingsStore from "../../../settings/SettingsStore";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import WidgetUtils from "../../../utils/WidgetUtils";
+import {SettingLevel} from "../../../settings/SettingLevel";
export default class WidgetOpenIDPermissionsDialog extends React.Component {
static propTypes = {
diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js
index e2ceadfbb9..5c01a6907f 100644
--- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js
+++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js
@@ -15,13 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { debounce } from 'lodash';
+import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
+import Field from '../../elements/Field';
+import AccessibleButton from '../../elements/AccessibleButton';
import { _t } from '../../../../languageHandler';
-import { accessSecretStorage } from '../../../../CrossSigningManager';
+
+// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
+// so this should be plenty and allow for people putting extra whitespace in the file because
+// maybe that's a thing people would do?
+const KEY_FILE_MAX_SIZE = 128;
+
+// Don't shout at the user that their key is invalid every time they type a key: wait a short time
+const VALIDATION_THROTTLE_MS = 200;
/*
* Access Secure Secret Storage by requesting the user's passphrase.
@@ -36,9 +47,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
+
+ this._fileUpload = React.createRef();
+
this.state = {
recoveryKey: "",
- recoveryKeyValid: false,
+ recoveryKeyValid: null,
+ recoveryKeyCorrect: null,
+ recoveryKeyFileError: null,
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
@@ -55,18 +71,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
- _onResetRecoveryClick = () => {
- // Re-enter the access flow, but resetting storage this time around.
- this.props.onFinished(false);
- accessSecretStorage(() => {}, /* forceReset = */ true);
+ _validateRecoveryKeyOnChange = debounce(() => {
+ this._validateRecoveryKey();
+ }, VALIDATION_THROTTLE_MS);
+
+ async _validateRecoveryKey() {
+ if (this.state.recoveryKey === '') {
+ this.setState({
+ recoveryKeyValid: null,
+ recoveryKeyCorrect: null,
+ });
+ return;
+ }
+
+ try {
+ const cli = MatrixClientPeg.get();
+ const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
+ const correct = await cli.checkSecretStorageKey(
+ decodedKey, this.props.keyInfo,
+ );
+ this.setState({
+ recoveryKeyValid: true,
+ recoveryKeyCorrect: correct,
+ });
+ } catch (e) {
+ this.setState({
+ recoveryKeyValid: false,
+ recoveryKeyCorrect: false,
+ });
+ }
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
- recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
- keyMatches: null,
+ recoveryKeyFileError: null,
});
+
+ // also clear the file upload control so that the user can upload the same file
+ // the did before (otherwise the onchange wouldn't fire)
+ if (this._fileUpload.current) this._fileUpload.current.value = null;
+
+ // We don't use Field's validation here because a) we want it in a separate place rather
+ // than in a tooltip and b) we want it to display feedback based on the uploaded file
+ // as well as the text box. Ideally we would refactor Field's validation logic so we could
+ // re-use some of it.
+ this._validateRecoveryKeyOnChange();
+ }
+
+ _onRecoveryKeyFileChange = async e => {
+ if (e.target.files.length === 0) return;
+
+ const f = e.target.files[0];
+
+ if (f.size > KEY_FILE_MAX_SIZE) {
+ this.setState({
+ recoveryKeyFileError: true,
+ recoveryKeyCorrect: false,
+ recoveryKeyValid: false,
+ });
+ } else {
+ const contents = await f.text();
+ // test it's within the base58 alphabet. We could be more strict here, eg. require the
+ // right number of characters, but it's really just to make sure that what we're reading is
+ // text because we'll put it in the text field.
+ if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
+ this.setState({
+ recoveryKeyFileError: null,
+ recoveryKey: contents.trim(),
+ });
+ this._validateRecoveryKey();
+ } else {
+ this.setState({
+ recoveryKeyFileError: true,
+ recoveryKeyCorrect: false,
+ recoveryKeyValid: false,
+ recoveryKey: '',
+ });
+ }
+ }
+ }
+
+ _onRecoveryKeyFileUploadClick = () => {
+ this._fileUpload.current.click();
}
_onPassPhraseNext = async (e) => {
@@ -106,6 +193,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
+ getKeyValidationText() {
+ if (this.state.recoveryKeyFileError) {
+ return _t("Wrong file type");
+ } else if (this.state.recoveryKeyCorrect) {
+ return _t("Looks good!");
+ } else if (this.state.recoveryKeyValid) {
+ return _t("Wrong Recovery Key");
+ } else if (this.state.recoveryKeyValid === null) {
+ return '';
+ } else {
+ return _t("Invalid Recovery Key");
+ }
+ }
+
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@@ -118,10 +219,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
let content;
let title;
+ let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
- title = _t("Enter recovery passphrase");
+ title = _t("Security Phrase");
+ titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
let keyStatus;
if (this.state.keyMatches === false) {
@@ -137,12 +240,15 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content =
{_t(
- "Warning: You should only do this on a trusted computer.", {},
- { b: sub => {sub} },
- )}
-
{_t(
- "Access your secure message history and your cross-signing " +
- "identity for verifying other sessions by entering your recovery passphrase.",
+ "Enter your Security Phrase or to continue.", {},
+ {
+ button: s =>
+ {s}
+ ,
+ },
)}
- {_t(
- "If you've forgotten your recovery passphrase you can "+
- "
use your recovery key or " +
- "
set up new recovery options."
- , {}, {
- button1: s =>
- {s}
- ,
- button2: s =>
- {s}
- ,
- })}
;
} else {
- title = _t("Enter recovery key");
+ title = _t("Security Key");
+ titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
- let keyStatus;
- if (this.state.recoveryKey.length === 0) {
- keyStatus = ;
- } else if (this.state.keyMatches === false) {
- keyStatus =
- {"\uD83D\uDC4E "}{_t(
- "Unable to access secret storage. " +
- "Please verify that you entered the correct recovery key.",
- )}
-
;
- } else if (this.state.recoveryKeyValid) {
- keyStatus =
- {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
-
;
- } else {
- keyStatus =
- {"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
-
;
- }
+ const feedbackClasses = classNames({
+ 'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
+ 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
+ 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
+ });
+ const recoveryKeyFeedback =
+ {this.getKeyValidationText()}
+
;
content =
-
{_t(
- "Warning: You should only do this on a trusted computer.", {},
- { b: sub => {sub} },
- )}
-
{_t(
- "Access your secure message history and your cross-signing " +
- "identity for verifying other sessions by entering your recovery key.",
- )}
+
{_t("Use your Security Key to continue.")}
-
- {_t(
- "If you've forgotten your recovery key you can "+
- "
."
- , {}, {
- button: s =>
- {s}
- ,
- })}
;
}
@@ -252,6 +333,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{content}
diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.tsx
similarity index 67%
rename from src/components/views/elements/AccessibleButton.js
rename to src/components/views/elements/AccessibleButton.tsx
index d708a44ab2..ae822204df 100644
--- a/src/components/views/elements/AccessibleButton.js
+++ b/src/components/views/elements/AccessibleButton.tsx
@@ -15,9 +15,36 @@
*/
import React from 'react';
-import PropTypes from 'prop-types';
import {Key} from '../../../Keyboard';
+import classnames from 'classnames';
+
+export type ButtonEvent = React.MouseEvent
| React.KeyboardEvent;
+
+/**
+ * children: React's magic prop. Represents all children given to the element.
+ * element: (optional) The base element type. "div" by default.
+ * onClick: (required) Event handler for button activation. Should be
+ * implemented exactly like a normal onClick handler.
+ */
+interface IProps extends React.InputHTMLAttributes {
+ inputRef?: React.Ref;
+ element?: string;
+ // The kind of button, similar to how Bootstrap works.
+ // See available classes for AccessibleButton for options.
+ kind?: string;
+ // The ARIA role
+ role?: string;
+ // The tabIndex
+ tabIndex?: number;
+ disabled?: boolean;
+ className?: string;
+ onClick?(e?: ButtonEvent): void;
+}
+
+interface IAccessibleButtonProps extends React.InputHTMLAttributes {
+ ref?: React.Ref;
+}
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@@ -27,11 +54,19 @@ import {Key} from '../../../Keyboard';
* @param {Object} props react element properties
* @returns {Object} rendered react
*/
-export default function AccessibleButton(props) {
- const {element, onClick, children, kind, disabled, ...restProps} = props;
-
+export default function AccessibleButton({
+ element,
+ onClick,
+ children,
+ kind,
+ disabled,
+ inputRef,
+ className,
+ ...restProps
+}: IProps) {
+ const newProps: IAccessibleButtonProps = restProps;
if (!disabled) {
- restProps.onClick = onClick;
+ newProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
@@ -39,7 +74,7 @@ export default function AccessibleButton(props) {
// And divs which we report as role button to assistive technologies.
// Browsers handle space and enter keypresses differently and we are only adjusting to the
// inconsistencies here
- restProps.onKeyDown = function(e) {
+ newProps.onKeyDown = (e) => {
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
@@ -50,7 +85,7 @@ export default function AccessibleButton(props) {
e.preventDefault();
}
};
- restProps.onKeyUp = function(e) {
+ newProps.onKeyUp = (e) => {
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
@@ -64,57 +99,26 @@ export default function AccessibleButton(props) {
}
// Pass through the ref - used for keyboard shortcut access to some buttons
- restProps.ref = restProps.inputRef;
- delete restProps.inputRef;
+ newProps.ref = inputRef;
- restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
-
- if (kind) {
- // We apply a hasKind class to maintain backwards compatibility with
- // buttons which might not know about kind and break
- restProps.className += " mx_AccessibleButton_hasKind mx_AccessibleButton_kind_" + kind;
- }
-
- if (disabled) {
- restProps.className += " mx_AccessibleButton_disabled";
- restProps["aria-disabled"] = true;
- }
+ newProps.className = classnames(
+ "mx_AccessibleButton",
+ className,
+ {
+ "mx_AccessibleButton_hasKind": kind,
+ [`mx_AccessibleButton_kind_${kind}`]: kind,
+ "mx_AccessibleButton_disabled": disabled,
+ },
+ );
+ // React.createElement expects InputHTMLAttributes
return React.createElement(element, restProps, children);
}
-/**
- * children: React's magic prop. Represents all children given to the element.
- * element: (optional) The base element type. "div" by default.
- * onClick: (required) Event handler for button activation. Should be
- * implemented exactly like a normal onClick handler.
- */
-AccessibleButton.propTypes = {
- children: PropTypes.node,
- inputRef: PropTypes.oneOfType([
- // Either a function
- PropTypes.func,
- // Or the instance of a DOM native element
- PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
- ]),
- element: PropTypes.string,
- onClick: PropTypes.func.isRequired,
-
- // The kind of button, similar to how Bootstrap works.
- // See available classes for AccessibleButton for options.
- kind: PropTypes.string,
- // The ARIA role
- role: PropTypes.string,
- // The tabIndex
- tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- disabled: PropTypes.bool,
-};
-
AccessibleButton.defaultProps = {
element: 'div',
role: 'button',
- tabIndex: "0",
+ tabIndex: 0,
};
AccessibleButton.displayName = "AccessibleButton";
diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.tsx
similarity index 57%
rename from src/components/views/elements/AccessibleTooltipButton.js
rename to src/components/views/elements/AccessibleTooltipButton.tsx
index 6c84c6ab7e..3546f62359 100644
--- a/src/components/views/elements/AccessibleTooltipButton.js
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -16,21 +16,28 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
+import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
-import * as sdk from "../../../index";
+import Tooltip from './Tooltip';
-export default class AccessibleTooltipButton extends React.PureComponent {
- static propTypes = {
- ...AccessibleButton.propTypes,
- // The tooltip to render on hover
- title: PropTypes.string.isRequired,
- };
+interface ITooltipProps extends React.ComponentProps {
+ title: string;
+ tooltip?: React.ReactNode;
+ tooltipClassName?: string;
+}
- state = {
- hover: false,
- };
+interface IState {
+ hover: boolean;
+}
+
+export default class AccessibleTooltipButton extends React.PureComponent {
+ constructor(props: ITooltipProps) {
+ super(props);
+ this.state = {
+ hover: false,
+ };
+ }
onMouseOver = () => {
this.setState({
@@ -38,25 +45,27 @@ export default class AccessibleTooltipButton extends React.PureComponent {
});
};
- onMouseOut = () => {
+ onMouseLeave = () => {
this.setState({
hover: false,
});
};
render() {
- const Tooltip = sdk.getComponent("elements.Tooltip");
- const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
-
- const {title, children, ...props} = this.props;
+ const {title, tooltip, children, tooltipClassName, ...props} = this.props;
const tip = this.state.hover ? : ;
return (
-
+
{ children }
{ tip }
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index 36af5059fc..e5ea2e5d20 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -58,18 +58,6 @@ export default createReactClass({
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
}
- // Removing networks for now as they're not really supported
- /*
- var network;
- if (this.props.networkUrl !== "") {
- network = (
-
-
-
- );
- }
- */
-
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js
index b96001b106..ec8bffc32f 100644
--- a/src/components/views/elements/AppPermission.js
+++ b/src/components/views/elements/AppPermission.js
@@ -1,7 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 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.
@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import url from 'url';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
+import SdkConfig from '../../../SdkConfig';
import WidgetUtils from "../../../utils/WidgetUtils";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@@ -76,6 +77,7 @@ export default class AppPermission extends React.Component {
}
render() {
+ const brand = SdkConfig.get().brand;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
@@ -96,7 +98,7 @@ export default class AppPermission extends React.Component {
{_t("Your avatar URL")}
{_t("Your user ID")}
{_t("Your theme")}
- {_t("Riot URL")}
+ {_t("%(brand)s URL", { brand })}
{_t("Room ID")}
{_t("Widget ID")}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 527436b0e4..d0fc56743f 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -29,16 +29,19 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
-import MessageSpinner from './MessageSpinner';
+import Spinner from './Spinner';
import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
-import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
+import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType";
+import {Capability} from "../../../widgets/WidgetApi";
+import {sleep} from "../../../utils/promise";
+import {SettingLevel} from "../../../settings/SettingLevel";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -341,23 +344,37 @@ export default class AppTile extends React.Component {
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
+ * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/
_endWidgetActions() {
- // HACK: This is a really dirty way to ensure that Jitsi cleans up
- // its hold on the webcam. Without this, the widget holds a media
- // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
- if (this._appFrame.current) {
- // In practice we could just do `+= ''` to trick the browser
- // into thinking the URL changed, however I can foresee this
- // being optimized out by a browser. Instead, we'll just point
- // the iframe at a page that is reasonably safe to use in the
- // event the iframe doesn't wink away.
- // This is relative to where the Riot instance is located.
- this._appFrame.current.src = 'about:blank';
+ let terminationPromise;
+
+ if (this._hasCapability(Capability.ReceiveTerminate)) {
+ // Wait for widget to terminate within a timeout
+ const timeout = 2000;
+ const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
+ terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
+ } else {
+ terminationPromise = Promise.resolve();
}
- // Delete the widget from the persisted store for good measure.
- PersistedElement.destroyElement(this._persistKey);
+ return terminationPromise.finally(() => {
+ // HACK: This is a really dirty way to ensure that Jitsi cleans up
+ // its hold on the webcam. Without this, the widget holds a media
+ // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
+ if (this._appFrame.current) {
+ // In practice we could just do `+= ''` to trick the browser
+ // into thinking the URL changed, however I can foresee this
+ // being optimized out by a browser. Instead, we'll just point
+ // the iframe at a page that is reasonably safe to use in the
+ // event the iframe doesn't wink away.
+ // This is relative to where the Riot instance is located.
+ this._appFrame.current.src = 'about:blank';
+ }
+
+ // Delete the widget from the persisted store for good measure.
+ PersistedElement.destroyElement(this._persistKey);
+ });
}
/* If user has permission to modify widgets, delete the widget,
@@ -381,12 +398,12 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
- this._endWidgetActions();
-
- WidgetUtils.setRoomWidget(
- this.props.room.roomId,
- this.props.app.id,
- ).catch((e) => {
+ this._endWidgetActions().then(() => {
+ return WidgetUtils.setRoomWidget(
+ this.props.room.roomId,
+ this.props.app.id,
+ );
+ }).catch((e) => {
console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@@ -669,6 +686,17 @@ export default class AppTile extends React.Component {
}
_onPopoutWidgetClick() {
+ // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
+ // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
+ if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
+ this._endWidgetActions().then(() => {
+ if (this._appFrame.current) {
+ // Reload iframe
+ this._appFrame.current.src = this._getRenderedUrl();
+ this.setState({});
+ }
+ });
+ }
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
@@ -677,6 +705,7 @@ export default class AppTile extends React.Component {
_onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
+ // eslint-disable-next-line no-self-assign
this._appFrame.current.src = this._appFrame.current.src;
}
@@ -713,7 +742,7 @@ export default class AppTile extends React.Component {
if (this.props.show) {
const loadingElement = (
-
+
);
if (!this.state.hasPermissionToLoad) {
diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
deleted file mode 100644
index 1410bdabdb..0000000000
--- a/src/components/views/elements/CreateRoomButton.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-
-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 PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-
-const CreateRoomButton = function(props) {
- const ActionButton = sdk.getComponent('elements.ActionButton');
- return (
-
- );
-};
-
-CreateRoomButton.propTypes = {
- size: PropTypes.string,
- tooltip: PropTypes.bool,
-};
-
-export default CreateRoomButton;
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
index 3096ac42f7..3397fd901c 100644
--- a/src/components/views/elements/Draggable.tsx
+++ b/src/components/views/elements/Draggable.tsx
@@ -17,20 +17,20 @@ limitations under the License.
import React from 'react';
interface IProps {
- className: string,
- dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
- onMouseUp: (event: MouseEvent) => void,
+ className: string;
+ dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState;
+ onMouseUp: (event: MouseEvent) => void;
}
interface IState {
- onMouseMove: (event: MouseEvent) => void,
- onMouseUp: (event: MouseEvent) => void,
- location: ILocationState,
+ onMouseMove: (event: MouseEvent) => void;
+ onMouseUp: (event: MouseEvent) => void;
+ location: ILocationState;
}
export interface ILocationState {
- currentX: number,
- currentY: number,
+ currentX: number;
+ currentY: number;
}
export default class Draggable extends React.Component {
@@ -58,13 +58,13 @@ export default class Draggable extends React.Component {
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
- }
+ };
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
- }
+ };
private onMouseMove(event: MouseEvent): void {
const newLocation = this.props.dragFunc(this.state.location, event);
@@ -75,7 +75,7 @@ export default class Draggable extends React.Component {
}
render() {
- return
+ return ;
}
}
\ No newline at end of file
diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js
index 50d5a3d10f..34e53906a2 100644
--- a/src/components/views/elements/EditableItemList.js
+++ b/src/components/views/elements/EditableItemList.js
@@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t} from '../../../languageHandler.js';
+import {_t} from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
new file mode 100644
index 0000000000..7d8b774955
--- /dev/null
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -0,0 +1,129 @@
+/*
+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 from 'react';
+import classnames from 'classnames';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+
+import * as Avatar from '../../../Avatar';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import EventTile from '../rooms/EventTile';
+
+interface IProps {
+ /**
+ * The text to be displayed in the message preview
+ */
+ message: string;
+
+ /**
+ * Whether to use the irc layout or not
+ */
+ useIRCLayout: boolean;
+
+ /**
+ * classnames to apply to the wrapper of the preview
+ */
+ className: string;
+}
+
+interface IState {
+ userId: string;
+ displayname: string;
+ avatar_url: string;
+}
+
+const AVATAR_SIZE = 32;
+
+export default class EventTilePreview extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ userId: "@erim:fink.fink",
+ displayname: "Erimayas Fink",
+ avatar_url: null,
+ };
+ }
+
+ async componentDidMount() {
+ // Fetch current user data
+ const client = MatrixClientPeg.get();
+ const userId = client.getUserId();
+ const profileInfo = await client.getProfileInfo(userId);
+ const avatar_url = Avatar.avatarUrlForUser(
+ {avatarUrl: profileInfo.avatar_url},
+ AVATAR_SIZE, AVATAR_SIZE, "crop");
+
+ this.setState({
+ userId,
+ displayname: profileInfo.displayname,
+ avatar_url,
+ });
+
+ }
+
+ private fakeEvent({userId, displayname, avatar_url}: IState) {
+ // Fake it till we make it
+ const event = new MatrixEvent(JSON.parse(`{
+ "type": "m.room.message",
+ "sender": "${userId}",
+ "content": {
+ "m.new_content": {
+ "msgtype": "m.text",
+ "body": "${this.props.message}",
+ "displayname": "${displayname}",
+ "avatar_url": "${avatar_url}"
+ },
+ "msgtype": "m.text",
+ "body": "${this.props.message}",
+ "displayname": "${displayname}",
+ "avatar_url": "${avatar_url}"
+ },
+ "unsigned": {
+ "age": 97
+ },
+ "event_id": "$9999999999999999999999999999999999999999999",
+ "room_id": "!999999999999999999:matrix.org"
+ }`));
+
+ // Fake it more
+ event.sender = {
+ name: displayname,
+ userId: userId,
+ getAvatarUrl: (..._) => {
+ return avatar_url;
+ },
+ };
+
+ return event;
+ }
+
+ public render() {
+ const event = this.fakeEvent(this.state);
+
+ let className = classnames(
+ this.props.className,
+ {
+ "mx_IRCLayout": this.props.useIRCLayout,
+ "mx_GroupLayout": !this.props.useIRCLayout,
+ }
+ );
+
+ return
+
+
;
+ }
+}
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 771d2182ea..d9fd59dc11 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { debounce } from 'lodash';
-import {IFieldState, IValidationResult} from "../elements/Validation";
+import {IFieldState, IValidationResult} from "./Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@@ -29,60 +29,78 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
-interface IProps extends React.InputHTMLAttributes {
+interface IProps {
// The field's ID, which binds the input and label together. Immutable.
- id?: string,
- // The element to create. Defaults to "input".
- // To define options for a select, use
- element?: "input" | "select" | "textarea",
+ id?: string;
// The field's type (when used as an ). Defaults to "text".
- type?: string,
+ type?: string;
// id of a
- );
- },
-});
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
new file mode 100644
index 0000000000..03e91fac62
--- /dev/null
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -0,0 +1,104 @@
+/*
+Copyright 2017 Travis Ralston
+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 SettingsStore from "../../../settings/SettingsStore";
+import { _t } from '../../../languageHandler';
+import ToggleSwitch from "./ToggleSwitch";
+import StyledCheckbox from "./StyledCheckbox";
+import { SettingLevel } from "../../../settings/SettingLevel";
+
+interface IProps {
+ // The setting must be a boolean
+ name: string;
+ level: SettingLevel;
+ roomId?: string; // for per-room settings
+ label?: string; // untranslated
+ isExplicit?: boolean;
+ // XXX: once design replaces all toggles make this the default
+ useCheckbox?: boolean;
+ disabled?: boolean;
+ onChange?(checked: boolean): void;
+}
+
+interface IState {
+ value: boolean;
+}
+
+export default class SettingsFlag extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ value: SettingsStore.getValueAt(
+ this.props.level,
+ this.props.name,
+ this.props.roomId,
+ this.props.isExplicit,
+ ),
+ };
+ }
+
+ private onChange = async (checked: boolean) => {
+ await this.save(checked);
+ this.setState({ value: checked });
+ if (this.props.onChange) this.props.onChange(checked);
+ };
+
+ private checkBoxOnChange = (e: React.ChangeEvent) => {
+ this.onChange(e.target.checked);
+ };
+
+ private save = async (val?: boolean) => {
+ await SettingsStore.setValue(
+ this.props.name,
+ this.props.roomId,
+ this.props.level,
+ val !== undefined ? val : this.state.value,
+ );
+ };
+
+ 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);
+
+ if (this.props.useCheckbox) {
+ return
+ {label}
+ ;
+ } else {
+ return (
+
+ {label}
+
+
+ );
+ }
+ }
+}
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
index f76a4684d3..a88c581d07 100644
--- a/src/components/views/elements/Slider.tsx
+++ b/src/components/views/elements/Slider.tsx
@@ -65,9 +65,9 @@ export default class Slider extends React.Component {
const intervalWidth = 1 / (values.length - 1);
- const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
+ const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
- return 100 * (closest - 1 + linearInterpolation) * intervalWidth
+ return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
}
@@ -87,7 +87,7 @@ export default class Slider extends React.Component {
selection =
+