Merge branch 'develop' into e2ee-history-visibility

This commit is contained in:
Robin Townsend 2021-07-13 23:07:11 -04:00
commit d568e92e06
921 changed files with 32884 additions and 19078 deletions

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import React, { useState } from "react";
import PropTypes from "prop-types";
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => {
const [isHovering, setIsHovering] = useState(false);
const hoveringProps = {
onMouseEnter: () => setIsHovering(true),

View file

@ -16,16 +16,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import Pill from "../elements/Pill";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import { makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import SettingsStore from "../../../settings/SettingsStore";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { isUrlPermitted } from '../../../HtmlUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps {
ev: MatrixEvent;
@ -69,7 +69,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
static propTypes = {
ev: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
}
};
render() {
const content: IBridgeStateEvent = this.props.ev.getContent();
@ -131,7 +131,9 @@ export default class BridgeTile extends React.PureComponent<IProps> {
const networkName = network.displayname || network.id;
let networkLink = <span>{networkName}</span>;
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
networkLink = <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
networkLink = (
<a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
);
}
networkItem = _t("Workspace: <networkLink/>", {}, {
networkLink: () => networkLink,
@ -140,7 +142,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
let channelLink = <span>{channelName}</span>;
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>;
}
const id = this.props.ev.getId();

View file

@ -16,12 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Spinner from '../elements/Spinner';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.settings.ChangeAvatar")
export default class ChangeAvatar extends React.Component {
@ -107,7 +107,7 @@ export default class ChangeAvatar extends React.Component {
return MatrixClientPeg.get().sendStateEvent(
self.props.room.roomId,
'm.room.avatar',
{url: url},
{ url: url },
'',
);
} else {
@ -154,7 +154,7 @@ export default class ChangeAvatar extends React.Component {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
}
let uploadSection;

View file

@ -18,9 +18,9 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.ChangeDisplayName")
export default class ChangeDisplayName extends React.Component {

View file

@ -18,7 +18,7 @@ limitations under the License.
import Field from "../elements/Field";
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import withValidation from '../elements/Validation';
@ -27,7 +27,7 @@ import * as sdk from "../../../index";
import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
const FIELD_OLD_PASSWORD = 'field_old_password';
@ -206,7 +206,7 @@ export default class ChangePassword extends React.Component {
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Passwords can't be empty"),
},
],
],
});
onChangeNewPassword = (ev) => {
@ -245,7 +245,7 @@ export default class ChangePassword extends React.Component {
},
invalid: () => _t("Passwords don't match"),
},
],
],
});
onClickChange = async (ev) => {

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.CrossSigningPanel")
export default class CrossSigningPanel extends React.PureComponent {
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
async _getUpdatedStatus() {
const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks();
const crossSigning = cli._crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage;
const crossSigning = cli.crypto.crossSigningInfo;
const secretStorage = cli.crypto.secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
@ -259,7 +259,7 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
</tr>
</tbody></table>
</tbody></table>
</details>
{errorSection}
{actionRow}

View file

@ -20,11 +20,11 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.DevicesPanel")
export default class DevicesPanel extends React.Component {
@ -58,7 +58,7 @@ export default class DevicesPanel extends React.Component {
MatrixClientPeg.get().getDevices().then(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});
this.setState({ devices: resp.devices || [] });
},
(error) => {
if (this._unmounted) { return; }
@ -70,12 +70,11 @@ export default class DevicesPanel extends React.Component {
console.error("Error loading sessions:", error);
errtxt = _t("Unable to load session list");
}
this.setState({deviceLoadError: errtxt});
this.setState({ deviceLoadError: errtxt });
},
);
}
/*
* compare two devices, sorting from most-recently-seen to least-recently-seen
* (and then, for stability, by device id)
@ -107,7 +106,7 @@ export default class DevicesPanel extends React.Component {
selectedDevices.splice(i, 1);
}
return {selectedDevices};
return { selectedDevices };
});
}
@ -141,7 +140,7 @@ export default class DevicesPanel extends React.Component {
body: _t("Click the button below to confirm deleting these sessions.", {
count: numDevices,
}),
continueText: _t("Delete sessions", {count: numDevices}),
continueText: _t("Delete sessions", { count: numDevices }),
continueKind: "danger",
},
};
@ -214,7 +213,7 @@ export default class DevicesPanel extends React.Component {
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) }
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length })}
</AccessibleButton>;
const classes = classNames(this.props.className, "mx_DevicesPanel");

View file

@ -19,10 +19,10 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {formatDate} from '../../../DateUtils';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { formatDate } from '../../../DateUtils';
import StyledCheckbox from '../elements/StyledCheckbox';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.DevicesPanelEntry")
export default class DevicesPanelEntry extends React.Component {

View file

@ -16,15 +16,14 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingLevel";
import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from '../elements/SettingsFlag';
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel = props => {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>

View file

@ -18,15 +18,15 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
import { formatBytes, formatCountLong } from "../../../utils/FormattingUtils";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
import {SettingLevel} from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SeshatResetDialog from '../dialogs/SeshatResetDialog';
import InlineSpinner from '../elements/InlineSpinner';
interface IState {
enabling: boolean;
@ -118,7 +118,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
onFinished: () => {},
}, null, /* priority = */ false, /* static = */ true,
);
}
};
private onEnable = async () => {
this.setState({
@ -130,7 +130,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
await EventIndexPeg.get().startCrawler();
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, true);
await this.updateState();
}
};
private confirmEventStoreReset = () => {
const { close } = Modal.createDialog(SeshatResetDialog, {
@ -143,11 +143,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
}
},
});
}
};
render() {
let eventIndexingSettings = null;
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const brand = SdkConfig.get().brand;
if (EventIndexPeg.get() !== null) {
@ -232,7 +231,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
<p>
{this.state.enabling
? <InlineSpinner />
: _t("Message search initilisation failed")
: _t("Message search initialisation failed")
}
</p>
{EventIndexPeg.error && (

View file

@ -20,8 +20,8 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import {Key} from "../../../Keyboard";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.IntegrationManager")
export default class IntegrationManager extends React.Component {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import SettingsStore from '../../../settings/SettingsStore';
import Modal from '../../../Modal';
import {
@ -30,9 +30,9 @@ import {
import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import {SettingLevel} from "../../../settings/SettingLevel";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { SettingLevel } from "../../../settings/SettingLevel";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
@ -40,7 +40,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
// TODO: this component also does a lot of direct poking into this.state, which
// is VERY NAUGHTY.
/**
* Rules that Vector used to set in order to override the actions of default rules.
* These are used to port peoples existing overrides to match the current API.
@ -100,7 +99,7 @@ export default class Notifications extends React.Component {
MatrixClientPeg.get().setPushRuleEnabled(
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
).then(function() {
self._refreshFromServer();
self._refreshFromServer();
});
};
@ -517,7 +516,7 @@ export default class Notifications extends React.Component {
};
// HS default rules
const defaultRules = {master: [], vector: {}, others: []};
const defaultRules = { master: [], vector: {}, others: [] };
for (const kind in rulesets.global) {
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
@ -580,12 +579,12 @@ export default class Notifications extends React.Component {
"vectorRuleId": "_keywords",
"description": (
<span>
{ _t('Messages containing <span>keywords</span>',
{},
{ 'span': (sub) =>
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
},
)}
{ _t('Messages containing <span>keywords</span>',
{},
{ 'span': (sub) =>
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
},
)}
</span>
),
"vectorState": self.state.vectorContentRules.vectorState,
@ -632,7 +631,7 @@ export default class Notifications extends React.Component {
});
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
self.setState({pushers: resp.pushers});
self.setState({ pushers: resp.pushers });
});
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
@ -655,7 +654,7 @@ export default class Notifications extends React.Component {
});
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids }));
};
_onClearNotifications = () => {
@ -743,8 +742,8 @@ export default class Notifications extends React.Component {
emailNotificationsRow(address, label) {
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} key={`emailNotif_${label}`} />;
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} key={`emailNotif_${label}`} />;
}
render() {
@ -757,8 +756,8 @@ export default class Notifications extends React.Component {
let masterPushRuleDiv;
if (this.state.masterPushRule) {
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')} />;
onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')} />;
}
let clearNotificationsButton;
@ -874,16 +873,16 @@ export default class Notifications extends React.Component {
{ spinner }
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications for this session')} />
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications for this session')} />
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange}
label={_t('Show message in desktop notification')} />
onChange={this.onEnableDesktopNotificationBodyChange}
label={_t('Show message in desktop notification')} />
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications for this session')} />
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications for this session')} />
{ emailNotificationsRows }

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import React, { createRef } from 'react';
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import { getHostingLink } from '../../../utils/HostingLink';
import * as sdk from "../../../index";
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.settings.ProfileSettings")
export default class ProfileSettings extends React.Component {
@ -79,7 +79,7 @@ export default class ProfileSettings extends React.Component {
e.preventDefault();
if (!this.state.enableProfileSave) return;
this.setState({enableProfileSave: false});
this.setState({ enableProfileSave: false });
const client = MatrixClientPeg.get();
const newState = {};
@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component {
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<input
type="file"
ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged}
accept="image/*"
/>
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
@ -26,7 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.SecureBackupPanel")
export default class SecureBackupPanel extends React.PureComponent {
@ -85,7 +85,7 @@ export default class SecureBackupPanel extends React.PureComponent {
async _checkKeyBackupStatus() {
this._getUpdatedDiagnostics();
try {
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
this.setState({
loading: false,
error: null,
@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
async _getUpdatedDiagnostics() {
const cli = MatrixClientPeg.get();
const secretStorage = cli._crypto._secretStorage;
const secretStorage = cli.crypto.secretStorage;
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();
@ -172,7 +172,7 @@ export default class SecureBackupPanel extends React.PureComponent {
danger: true,
onFinished: (proceed) => {
if (!proceed) return;
this.setState({loading: true});
this.setState({ loading: true });
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
this._loadBackupStatus();
});
@ -237,7 +237,7 @@ export default class SecureBackupPanel extends React.PureComponent {
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.", {},
{b: sub => <b>{sub}</b>},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Connect this session to key backup before signing out to avoid " +
@ -385,7 +385,7 @@ export default class SecureBackupPanel extends React.PureComponent {
statusDescription = <>
<p>{_t(
"Your keys are <b>not being backed up from this session</b>.", {},
{b: sub => <b>{sub}</b>},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</>;

View file

@ -16,18 +16,21 @@ limitations under the License.
import url from 'url';
import React from 'react';
import {_t} from "../../../languageHandler";
import * as sdk from '../../../index';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import dis from "../../../dispatcher/dispatcher";
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { abbreviateUrl, unabbreviateUrl } from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import {timeout} from "../../../utils/promise";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { timeout } from "../../../utils/promise";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads';
import InlineSpinner from '../elements/InlineSpinner';
import AccessibleButton from '../elements/AccessibleButton';
import Field from '../elements/Field';
import QuestionDialog from "../dialogs/QuestionDialog";
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@ -50,7 +53,7 @@ async function checkIdentityServerUrl(u) {
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status});
return _t("Not a valid Identity Server (status code %(code)s)", { code: response.status });
} else {
return _t("Could not connect to Identity Server");
}
@ -121,12 +124,11 @@ export default class SetIdServer extends React.Component<IProps, IState> {
private onIdentityServerChanged = (ev) => {
const u = ev.target.value;
this.setState({idServer: u});
this.setState({ idServer: u });
};
private getTooltip = () => {
if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div>
<InlineSpinner />
{ _t("Checking server") }
@ -159,14 +161,14 @@ export default class SetIdServer extends React.Component<IProps, IState> {
e.preventDefault();
const { idServer, currentClientIdServer } = this.state;
this.setState({busy: true, checking: true, error: null});
this.setState({ busy: true, checking: true, error: null });
const fullUrl = unabbreviateUrl(idServer);
let errStr = await checkIdentityServerUrl(fullUrl);
if (!errStr) {
try {
this.setState({checking: false}); // clear tooltip
this.setState({ checking: false }); // clear tooltip
// Test the identity server by trying to register with it. This
// may result in a terms of service prompt.
@ -217,7 +219,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
};
private showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
@ -236,13 +237,13 @@ export default class SetIdServer extends React.Component<IProps, IState> {
}
private onDisconnectClicked = async () => {
this.setState({disconnectBusy: true});
this.setState({ disconnectBusy: true });
try {
const [confirmed] = await this.showServerChangeWarning({
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {},
{idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>},
{ idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b> },
),
button: _t("Disconnect"),
});
@ -250,7 +251,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
this.disconnectIdServer();
}
} finally {
this.setState({disconnectBusy: false});
this.setState({ disconnectBusy: false });
}
};
@ -319,7 +320,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
message = unboundMessage;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, {
title,
description: message,
@ -352,8 +352,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
};
render() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const Field = sdk.getComponent('elements.Field');
const idServerUrl = this.state.currentClientIdServer;
let sectionTitle;
let bodyText;
@ -369,7 +367,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
bodyText = _t(
"If you don't want to use <server /> to discover and be discoverable by existing " +
"contacts you know, enter another identity server below.",
{}, {server: sub => <b>{abbreviateUrl(idServerUrl)}</b>},
{}, { server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
);
}
} else {
@ -398,7 +396,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
discoButtonContent = _t("Do not use an identity server");
}
if (this.state.disconnectBusy) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
discoButtonContent = <InlineSpinner />;
}
discoSection = <div>

View file

@ -15,17 +15,27 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../languageHandler";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import { _t } from "../../../languageHandler";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
import * as sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
}
interface IState {
currentManager: IntegrationManagerInstance;
provisioningEnabled: boolean;
}
@replaceableComponent("views.settings.SetIntegrationManager")
export default class SetIntegrationManager extends React.Component {
constructor() {
super();
export default class SetIntegrationManager extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager();
@ -35,18 +45,18 @@ export default class SetIntegrationManager extends React.Component {
};
}
onProvisioningToggled = () => {
private onProvisioningToggled = (): void => {
const current = this.state.provisioningEnabled;
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
console.error("Error changing integration manager provisioning");
console.error(err);
this.setState({provisioningEnabled: current});
this.setState({ provisioningEnabled: current });
});
this.setState({provisioningEnabled: !current});
this.setState({ provisioningEnabled: !current });
};
render() {
public render(): React.ReactNode {
const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch");
const currentManager = this.state.currentManager;
@ -57,7 +67,7 @@ export default class SetIntegrationManager extends React.Component {
bodyText = _t(
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{serverName: currentManager.name},
{ serverName: currentManager.name },
{ b: sub => <b>{sub}</b> },
);
} else {

View file

@ -17,21 +17,21 @@ limitations under the License.
import React from 'react';
import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface ExistingSpellCheckLanguageIProps {
language: string,
onRemoved(language: string),
language: string;
onRemoved(language: string);
}
interface SpellCheckLanguagesIProps {
languages: Array<string>,
onLanguagesChange(languages: Array<string>),
languages: Array<string>;
onLanguagesChange(languages: Array<string>);
}
interface SpellCheckLanguagesIState {
newLanguage: string,
newLanguage: string;
}
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
@ -60,7 +60,7 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
super(props);
this.state = {
newLanguage: "",
}
};
}
_onRemoved = (language) => {
@ -77,13 +77,13 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
if (!language) return;
if (this.props.languages.includes(language)) return;
this.props.languages.push(language)
this.props.languages.push(language);
this.props.onLanguagesChange(this.props.languages);
};
_onNewLanguageChange = (language: string) => {
if (this.state.newLanguage === language) return;
this.setState({newLanguage: language});
this.setState({ newLanguage: language });
};
render() {

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import React, { useState } from "react";
import {UpdateCheckStatus} from "../../../BasePlatform";
import { UpdateCheckStatus } from "../../../BasePlatform";
import PlatformPeg from "../../../PlatformPeg";
import {useDispatcher} from "../../../hooks/useDispatcher";
import { useDispatcher } from "../../../hooks/useDispatcher";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {_t} from "../../../languageHandler";
import { Action } from "../../../dispatcher/actions";
import { _t } from "../../../languageHandler";
import InlineSpinner from "../../../components/views/elements/InlineSpinner";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import {CheckUpdatesPayload} from "../../../dispatcher/payloads/CheckUpdatesPayload";
import { CheckUpdatesPayload } from "../../../dispatcher/payloads/CheckUpdatesPayload";
function installUpdate() {
PlatformPeg.get().installUpdate();
@ -61,7 +61,7 @@ const UpdateCheckButton = () => {
PlatformPeg.get().startUpdateCheck();
};
useDispatcher(dis, ({action, ...params}) => {
useDispatcher(dis, ({ action, ...params }) => {
if (action === Action.CheckUpdates) {
setState(params as CheckUpdatesPayload);
}

View file

@ -17,15 +17,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import * as Email from "../../../../email";
import AddThreepid from "../../../../AddThreepid";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal';
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../utils/replaceableComponent";
/*
TODO: Improve the UX for everything in here.
@ -57,14 +57,14 @@ export class ExistingEmailAddress extends React.Component {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
this.setState({ verifyRemove: true });
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
_onActuallyRemove = (e) => {
@ -88,14 +88,20 @@ export class ExistingEmailAddress extends React.Component {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Remove %(email)s?", {email: this.props.email.address} )}
{_t("Remove %(email)s?", { email: this.props.email.address } )}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn">
<AccessibleButton
onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Remove")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn">
<AccessibleButton
onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Cancel")}
</AccessibleButton>
</div>
@ -161,13 +167,13 @@ export default class EmailAddresses extends React.Component {
}
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
this.setState({ verifying: true, continueDisabled: true, addTask: task });
task.addEmailAddress(email).then(() => {
this.setState({continueDisabled: false});
this.setState({ continueDisabled: false });
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
@ -179,7 +185,7 @@ export default class EmailAddresses extends React.Component {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
this.setState({ continueDisabled: true });
this.state.addTask.checkEmailLinkClicked().then(([finished]) => {
let newEmailAddress = this.state.newEmailAddress;
if (finished) {
@ -198,7 +204,7 @@ export default class EmailAddresses extends React.Component {
newEmailAddress,
});
}).catch((err) => {
this.setState({continueDisabled: false});
this.setState({ continueDisabled: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component {
);
if (this.state.verifying) {
addButton = (
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</div>
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<AccessibleButton
onClick={this._onContinueClick}
kind="primary"
disabled={this.state.continueDisabled}
>
{_t("Continue")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete="off"
noValidate={true} className="mx_EmailAddresses_new">
<form
onSubmit={this._onAddClick}
autoComplete="off"
noValidate={true}
className="mx_EmailAddresses_new"
>
<Field
type="text"
label={_t("Email Address")}

View file

@ -17,15 +17,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import AddThreepid from "../../../../AddThreepid";
import CountryDropdown from "../../auth/CountryDropdown";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal';
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../utils/replaceableComponent";
/*
TODO: Improve the UX for everything in here.
@ -52,14 +52,14 @@ export class ExistingPhoneNumber extends React.Component {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
this.setState({ verifyRemove: true });
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
_onActuallyRemove = (e) => {
@ -83,14 +83,20 @@ export class ExistingPhoneNumber extends React.Component {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{_t("Remove %(phone)s?", {phone: this.props.msisdn.address})}
{_t("Remove %(phone)s?", { phone: this.props.msisdn.address })}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
<AccessibleButton
onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Remove")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
<AccessibleButton
onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Cancel")}
</AccessibleButton>
</div>
@ -158,13 +164,13 @@ export default class PhoneNumbers extends React.Component {
const phoneCountry = this.state.phoneCountry;
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
this.setState({ verifying: true, continueDisabled: true, addTask: task });
task.addMsisdn(phoneCountry, phoneNumber).then((response) => {
this.setState({continueDisabled: false, verifyMsisdn: response.msisdn});
this.setState({ continueDisabled: false, verifyMsisdn: response.msisdn });
}).catch((err) => {
console.error("Unable to add phone number " + phoneNumber + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
title: _t("Error"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
@ -176,7 +182,7 @@ export default class PhoneNumbers extends React.Component {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
this.setState({ continueDisabled: true });
const token = this.state.newPhoneNumberCode;
const address = this.state.verifyMsisdn;
this.state.addTask.haveMsisdnToken(token).then(([finished]) => {
@ -199,7 +205,7 @@ export default class PhoneNumbers extends React.Component {
newPhoneNumberCode: "",
});
}).catch((err) => {
this.setState({continueDisabled: false});
this.setState({ continueDisabled: false });
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify phone number: " + err);
@ -208,13 +214,13 @@ export default class PhoneNumbers extends React.Component {
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
} else {
this.setState({verifyError: _t("Incorrect verification code")});
this.setState({ verifyError: _t("Incorrect verification code") });
}
});
};
_onCountryChanged = (e) => {
this.setState({phoneCountry: e.iso2});
this.setState({ phoneCountry: e.iso2 });
};
render() {
@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component {
value={this.state.newPhoneNumberCode}
onChange={this._onChangeNewPhoneNumberCode}
/>
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
<AccessibleButton
onClick={this._onContinueClick}
kind="primary"
disabled={this.state.continueDisabled}
>
{_t("Continue")}
</AccessibleButton>
</form>

View file

@ -19,11 +19,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../languageHandler";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../utils/replaceableComponent";
/*
TODO: Improve the UX for everything in here.

View file

@ -19,11 +19,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../../languageHandler";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import * as sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../utils/replaceableComponent";
/*
TODO: Improve the UX for everything in here.
@ -190,7 +190,7 @@ export class PhoneNumber extends React.Component {
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
} else {
this.setState({verifyError: _t("Incorrect verification code")});
this.setState({ verifyError: _t("Incorrect verification code") });
}
}
}

View file

@ -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<IProps, IState> {
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<IState> = {};
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');
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
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 = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
}
let roomUpgradeButton;
@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
roomUpgradeButton = (
<div>
<p className='mx_SettingsTab_warningText'>
{_t(
{ _t(
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
"to the new version of the room.</i> 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) => <b>{sub}</b>,
"i": (sub) => <i>{sub}</i>,
},
)}
) }
</p>
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
{_t("Upgrade this room to the recommended room version")}
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
{ _t("Upgrade this room to the recommended room version") }
</AccessibleButton>
</div>
);
}
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 = (
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
{_t("View older messages in %(roomName)s.", {roomName: name})}
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
<span className='mx_SettingsTab_subheading'>
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
</span>
<div>
<span>{_t("Internal room ID:")}</span>&nbsp;
{this.props.roomId}
<span>{ _t("Internal room ID:") }</span>&nbsp;
{ this.props.roomId }
</div>
{unfederatableSection}
{ unfederatableSection }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
<div>
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
<span>{ _t("Room version:") }</span>&nbsp;
{ room.getVersion() }
</div>
{oldRoomLink}
{roomUpgradeButton}
{ oldRoomLink }
{ roomUpgradeButton }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
<AccessibleButton onClick={this._openDevtools} kind='primary'>
{_t("Open Devtools")}
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
<AccessibleButton onClick={this.openDevtools} kind='primary'>
{ _t("Open Devtools") }
</AccessibleButton>
</div>
</div>

View file

@ -15,13 +15,13 @@ limitations under the License.
*/
import React from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import BridgeTile from "../../BridgeTile";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
const BRIDGE_EVENT_TYPES = [
"uk.half-shot.bridge",
@ -44,14 +44,11 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
return <BridgeTile key={event.getId()} room={room} ev={event} />;
}
static getBridgeStateEvents(roomId: string) {
static getBridgeStateEvents(roomId: string): MatrixEvent[] {
const client = MatrixClientPeg.get();
const roomState = client.getRoom(roomId).currentState;
return BRIDGE_EVENT_TYPES.map(typeName => {
const events = roomState.events.get(typeName);
return events ? Array.from(events.values()) : [];
}).flat(1);
return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1);
}
render() {

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
export default class GeneralRoomSettingsTab extends React.Component {
@ -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", "");
@ -80,9 +79,11 @@ export default class GeneralRoomSettingsTab extends React.Component {
flairSection = <>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
<RelatedGroupSettings
roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent}
/>
</div>
</>;
}
@ -97,8 +98,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} />
</div>
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
{ flairSection }

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
export default class NotificationsSettingsTab extends React.Component {
@ -47,7 +47,7 @@ export default class NotificationsSettingsTab extends React.Component {
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
this.setState({ currentSound: soundData.name || soundData.url });
}
async _triggerUploader(e) {
@ -155,7 +155,7 @@ export default class NotificationsSettingsTab extends React.Component {
<div>
<span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
{_t("Reset")}
{_t("Reset")}
</AccessibleButton>
</div>
<div>
@ -167,11 +167,11 @@ export default class NotificationsSettingsTab extends React.Component {
{currentUploadedFile}
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
{_t("Browse")}
{_t("Browse")}
</AccessibleButton>
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
{_t("Save")}
{_t("Save")}
</AccessibleButton>
<br />
</div>

View file

@ -15,16 +15,18 @@ limitations under the License.
*/
import React from 'react';
import {_t, _td} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import {EventType} from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { compare } from "../../../../../utils/strings";
import ErrorDialog from '../../../dialogs/ErrorDialog';
import PowerSelector from "../../../elements/PowerSelector";
const plEventsToLabels = {
// These will be translated for us later.
@ -44,18 +46,18 @@ const plEventsToLabels = {
const plEventsToShow = {
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
[EventType.RoomAvatar]: {isState: true},
[EventType.RoomName]: {isState: true},
[EventType.RoomCanonicalAlias]: {isState: true},
[EventType.RoomHistoryVisibility]: {isState: true},
[EventType.RoomPowerLevels]: {isState: true},
[EventType.RoomTopic]: {isState: true},
[EventType.RoomTombstone]: {isState: true},
[EventType.RoomEncryption]: {isState: true},
[EventType.RoomServerAcl]: {isState: true},
[EventType.RoomAvatar]: { isState: true },
[EventType.RoomName]: { isState: true },
[EventType.RoomCanonicalAlias]: { isState: true },
[EventType.RoomHistoryVisibility]: { isState: true },
[EventType.RoomPowerLevels]: { isState: true },
[EventType.RoomTopic]: { isState: true },
[EventType.RoomTombstone]: { isState: true },
[EventType.RoomEncryption]: { isState: true },
[EventType.RoomServerAcl]: { isState: true },
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": {isState: true},
"im.vector.modular.widgets": { isState: true },
};
// parse a string as an integer; if the input is undefined, or cannot be parsed
@ -75,7 +77,6 @@ interface IBannedUserProps {
export class BannedUser extends React.Component<IBannedUserProps> {
private onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
title: _t('Error'),
@ -102,7 +103,7 @@ export class BannedUser extends React.Component<IBannedUserProps> {
return (
<li>
{unbanButton}
<span title={_t("Banned by %(displayName)s", {displayName: this.props.by})}>
<span title={_t("Banned by %(displayName)s", { displayName: this.props.by })}>
<strong>{ this.props.member.name }</strong> {userId}
{this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : ""}
</span>
@ -175,7 +176,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, {
title: _t('Error changing power level requirement'),
description: _t(
@ -202,7 +202,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Power level change failed', '', ErrorDialog, {
title: _t('Error changing power level'),
description: _t(
@ -214,8 +213,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
};
render() {
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
@ -283,6 +280,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const mutedUsers = [];
Object.keys(userLevels).forEach((user) => {
if (!Number.isInteger(userLevels[user])) { return; }
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(
@ -312,7 +310,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
const comparator = (a, b) => {
const plDiff = userLevels[b.key] - userLevels[a.key];
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase());
};
privilegedUsers.sort(comparator);
@ -393,7 +391,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (label) {
label = _t(label);
} else {
label = _t("Send %(eventType)s events", {eventType});
label = _t("Send %(eventType)s events", { eventType });
}
return (
<div className="" key={eventType}>

View file

@ -16,32 +16,35 @@ limitations under the License.
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
// Knock and private are reserved keywords which are not yet implemented.
enum JoinRule {
export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
/**
* @deprecated Reserved. Should not be used.
*/
Private = "private",
}
enum GuestAccess {
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
enum HistoryVisibility {
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@ -97,9 +100,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
HistoryVisibility.Shared,
);
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
this.setState({joinRule, guestAccess, history, encrypted});
this.setState({ joinRule, guestAccess, history, encrypted });
const hasAliases = await this.hasAliases();
this.setState({hasAliases});
this.setState({ hasAliases });
}
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
@ -121,7 +124,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
};
private onEncryptionChange = (e: React.ChangeEvent) => {
private onEncryptionChange = () => {
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'),
description: _t(
@ -137,18 +140,18 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
),
onFinished: (confirm) => {
if (!confirm) {
this.setState({encrypted: false});
this.setState({ encrypted: false });
return;
}
const beforeEncrypted = this.state.encrypted;
this.setState({encrypted: true});
this.setState({ encrypted: true });
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.encryption",
{ algorithm: "m.megolm.v1.aes-sha2" },
).catch((e) => {
console.error(e);
this.setState({encrypted: beforeEncrypted});
this.setState({ encrypted: beforeEncrypted });
});
},
});
@ -163,16 +166,26 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({joinRule, guestAccess});
this.setState({ joinRule, guestAccess });
const client = MatrixClientPeg.get();
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: joinRule}, "").catch((e) => {
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
console.error(e);
this.setState({joinRule: beforeJoinRule});
this.setState({ joinRule: beforeJoinRule });
});
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: guestAccess}, "").catch((e) => {
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
console.error(e);
this.setState({guestAccess: beforeGuestAccess});
this.setState({ guestAccess: beforeGuestAccess });
});
};
@ -209,27 +222,37 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({joinRule, guestAccess});
this.setState({ joinRule, guestAccess });
const client = MatrixClientPeg.get();
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: joinRule}, "").catch((e) => {
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
console.error(e);
this.setState({joinRule: beforeJoinRule});
this.setState({ joinRule: beforeJoinRule });
});
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: guestAccess}, "").catch((e) => {
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
console.error(e);
this.setState({guestAccess: beforeGuestAccess});
this.setState({ guestAccess: beforeGuestAccess });
});
};
private onHistoryRadioToggle = (history: HistoryVisibility) => {
const beforeHistory = this.state.history;
this.setState({history: history});
this.setState({ history: history });
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
history_visibility: history,
}, "").catch((e) => {
console.error(e);
this.setState({history: beforeHistory});
this.setState({ history: beforeHistory });
});
};
@ -368,8 +391,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
render() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isEncrypted = this.state.encrypted;

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import SettingsStore from "../../../../../settings/SettingsStore";
@ -35,9 +35,10 @@ import Field from '../../../elements/Field';
import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
import {Layout} from "../../../../../settings/Layout";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/Layout";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { compare } from "../../../../../utils/strings";
interface IProps {
}
@ -83,7 +84,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
fontSize: (SettingsStore.getValue("baseFontSize", null) + FontWatcher.SIZE_DIFF).toString(),
...this.calculateThemeState(),
customThemeUrl: "",
customThemeMessage: {isError: false, text: ""},
customThemeMessage: { isError: false, text: "" },
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
useSystemFont: SettingsStore.getValue("useSystemFont"),
systemFont: SettingsStore.getValue("systemFont"),
@ -148,43 +149,43 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
// so remember what the value was before we tried to set it so we can revert
const oldTheme: string = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => {
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
this.setState({theme: oldTheme});
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme });
this.setState({ theme: oldTheme });
});
this.setState({theme: newTheme});
this.setState({ theme: newTheme });
// The settings watcher doesn't fire until the echo comes back from the
// server, so to make the theme change immediately we need to manually
// do the dispatch now
// XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override
// the value from settings.
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme, forceTheme: newTheme});
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme, forceTheme: newTheme });
};
private onUseSystemThemeChanged = (checked: boolean): void => {
this.setState({useSystemTheme: checked});
this.setState({ useSystemTheme: checked });
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
dis.dispatch<RecheckThemePayload>({ action: Action.RecheckTheme });
};
private onFontSizeChanged = (size: number): void => {
this.setState({fontSize: size.toString()});
this.setState({ fontSize: size.toString() });
SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, size - FontWatcher.SIZE_DIFF);
};
private onValidateFontSize = async ({value}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
private onValidateFontSize = async ({ value }: Pick<IFieldState, "value">): Promise<IValidationResult> => {
const parsedSize = parseFloat(value);
const min = FontWatcher.MIN_SIZE + FontWatcher.SIZE_DIFF;
const max = FontWatcher.MAX_SIZE + FontWatcher.SIZE_DIFF;
if (isNaN(parsedSize)) {
return {valid: false, feedback: _t("Size must be a number")};
return { valid: false, feedback: _t("Size must be a number") };
}
if (!(min <= parsedSize && parsedSize <= max)) {
return {
valid: false,
feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max}),
feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', { min, max }),
};
}
@ -195,7 +196,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
parseInt(value, 10) - FontWatcher.SIZE_DIFF,
);
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
return { valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', { min, max }) };
};
private onAddCustomTheme = async (): Promise<void> => {
@ -212,37 +213,37 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
// XXX: need some schema for this
const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
this.setState({ customThemeMessage: { text: _t("Invalid theme schema."), isError: true } });
return;
}
currentThemes.push(themeInfo);
} catch (e) {
console.error(e);
this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
this.setState({ customThemeMessage: { text: _t("Error downloading theme information."), isError: true } });
return; // Don't continue on error
}
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } });
this.themeTimer = setTimeout(() => {
this.setState({customThemeMessage: {text: "", isError: false}});
this.setState({ customThemeMessage: { text: "", isError: false } });
}, 3000);
};
private onCustomThemeChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void => {
this.setState({customThemeUrl: e.target.value});
this.setState({ customThemeUrl: e.target.value });
};
private onIRCLayoutChange = (enabled: boolean) => {
if (enabled) {
this.setState({layout: Layout.IRC});
this.setState({ layout: Layout.IRC });
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
} else {
this.setState({layout: Layout.Group});
this.setState({ layout: Layout.Group });
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
}
}
};
private renderThemeSection() {
const themeWatcher = new ThemeWatcher();
@ -292,10 +293,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
// XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
.map(p => ({ id: p[0], name: p[1] })); // convert pairs to objects for code readability
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p))
.sort((a, b) => a.name.localeCompare(b.name));
.sort((a, b) => compare(a.name, b.name));
const orderedThemes = [...builtInThemes, ...customThemes];
return (
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
@ -347,7 +348,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
<SettingsFlag
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked) => this.setState({useCustomFontSize: checked})}
onChange={(checked) => this.setState({ useCustomFontSize: checked })}
useCheckbox={true}
/>
@ -359,7 +360,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
value={this.state.fontSize.toString()}
id="font_size_field"
onValidate={this.onValidateFontSize}
onChange={(value) => this.setState({fontSize: value.target.value})}
onChange={(value) => this.setState({ fontSize: value.target.value })}
disabled={!this.state.useCustomFontSize}
className="mx_SettingsTab_customFontSizeField"
/>
@ -372,7 +373,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
const brand = SdkConfig.get().brand;
const toggle = <div
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}
>
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
</div>;
@ -402,7 +403,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
name="useSystemFont"
level={SettingLevel.DEVICE}
useCheckbox={true}
onChange={(checked) => this.setState({useSystemFont: checked})}
onChange={(checked) => this.setState({ useSystemFont: checked })}
/>
<Field
className="mx_AppearanceUserSettingsTab_systemFont"

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import GroupUserSettings from "../../../groups/GroupUserSettings";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.FlairUserSettingsTab")
export default class FlairUserSettingsTab extends React.Component {

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
import * as languageHandler from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
@ -27,19 +27,19 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types";
import PlatformPeg from "../../../../../PlatformPeg";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import { Service, startTermsFlow } from "../../../../../Terms";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
export default class GeneralUserSettingsTab extends React.Component {
@ -84,7 +84,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// the enabled flag value.
const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false;
this.setState({serverSupportsSeparateAddAndBind, canChangePassword});
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
this._getThreepidState();
}
@ -104,7 +104,7 @@ export default class GeneralUserSettingsTab extends React.Component {
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
this._getThreepidState();
}
};
@ -145,7 +145,7 @@ export default class GeneralUserSettingsTab extends React.Component {
async _checkTerms() {
if (!this.state.haveIdServer) {
this.setState({idServerHasUnsignedTerms: false});
this.setState({ idServerHasUnsignedTerms: false });
return;
}
@ -191,7 +191,7 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.language === newLanguage) return;
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({language: newLanguage});
this.setState({ language: newLanguage });
const platform = PlatformPeg.get();
if (platform) {
platform.setLanguage(newLanguage);
@ -200,7 +200,7 @@ export default class GeneralUserSettingsTab extends React.Component {
};
_onSpellCheckLanguagesChange = (languages) => {
this.setState({spellCheckLanguages: languages});
this.setState({ spellCheckLanguages: languages });
const plaf = PlatformPeg.get();
if (plaf) {
@ -323,8 +323,11 @@ export default class GeneralUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<LanguageDropdown className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} value={this.state.language} />
<LanguageDropdown
className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange}
value={this.state.language}
/>
</div>
);
}
@ -333,8 +336,10 @@ export default class GeneralUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
<SpellCheckSettings languages={this.state.spellCheckLanguages}
onLanguagesChange={this._onSpellCheckLanguagesChange} />
<SpellCheckSettings
languages={this.state.spellCheckLanguages}
onLanguagesChange={this._onSpellCheckLanguagesChange}
/>
</div>
);
}
@ -348,7 +353,7 @@ export default class GeneralUserSettingsTab extends React.Component {
{_t(
"Agree to the identity server (%(serverName)s) Terms of Service to " +
"allow yourself to be discoverable by email address or phone number.",
{serverName: this.state.idServerName},
{ serverName: this.state.idServerName },
)}
</span>;
return (

View file

@ -15,20 +15,25 @@ limitations under the License.
*/
import React from 'react';
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../../createRoom";
import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import UpdateCheckButton from "../../UpdateCheckButton";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { copyPlaintext } from "../../../../../utils/strings";
import * as ContextMenu from "../../../../structures/ContextMenu";
import { toRightOf } from "../../../../structures/ContextMenu";
import BugReportDialog from '../../../dialogs/BugReportDialog';
import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu";
interface IProps {
closeSettingsFn: () => {};
closeSettingsFn: () => void;
}
interface IState {
@ -38,6 +43,8 @@ interface IState {
@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
export default class HelpUserSettingsTab extends React.Component<IProps, IState> {
protected closeCopiedTooltip: () => void;
constructor(props) {
super(props);
@ -48,14 +55,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
}
componentDidMount(): void {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({appVersion: ver})).catch((e) => {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({ appVersion: ver })).catch((e) => {
console.error("Error getting vector version: ", e);
});
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => {
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({ canUpdate: v })).catch((e) => {
console.error("Error getting self updatability: ", e);
});
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
private onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return;
@ -69,10 +82,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
};
private onBugReport = (e) => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
@ -153,6 +162,19 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
);
}
onAccessTokenCopyClick = async (e) => {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
const buttonRect = target.getBoundingClientRect();
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
this.closeCopiedTooltip = target.onmouseleave = close;
};
render() {
const brand = SdkConfig.get().brand;
@ -269,12 +291,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{_t("Access Token:") + ' '}
<AccessibleButton element="span" onClick={this.showSpoiler}
data-spoiler={MatrixClientPeg.get().getAccessToken()}
>
&lt;{ _t("click to reveal") }&gt;
</AccessibleButton>
<br />
<details>
<summary>{_t("Access Token")}</summary><br />
<b>{_t("Your access token gives full access to your account."
+ " Do not share it with anyone." )}</b>
<div className="mx_HelpUserSettingsTab_accessToken">
<code>{MatrixClientPeg.get().getAccessToken()}</code>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onAccessTokenCopyClick}
className="mx_HelpUserSettingsTab_accessToken_copy"
/>
</div>
</details><br />
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}

View file

@ -15,13 +15,15 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import PropTypes from "prop-types";
import SettingsStore from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as sdk from "../../../../../index";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
export class LabsSettingToggle extends React.Component {
static propTypes = {
@ -48,14 +50,40 @@ export default class LabsUserSettingsTab extends React.Component {
}
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = SettingsStore.getFeatureSettingNames().map(f => <LabsSettingToggle featureId={f} key={f} />);
const features = SettingsStore.getFeatureSettingNames();
const [labs, betas] = features.reduce((arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
return arr;
}, [[], []]);
let betaSection;
if (betas.length) {
betaSection = <div className="mx_SettingsTab_section">
{ betas.map(f => <BetaCard key={f} featureId={f} /> ) }
</div>;
}
let labsSection;
if (SdkConfig.get()['showLabsSettings']) {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
labsSection = <div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
<SettingsFlag name="advancedRoomListLogging" level={SettingLevel.DEVICE} />
</div>;
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t('Customise your experience with experimental labs features. ' +
_t('Feeling experimental? Labs are the best way to get things early, ' +
'test out new features and help shape them before they actually launch. ' +
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
@ -64,13 +92,8 @@ export default class LabsUserSettingsTab extends React.Component {
})
}
</div>
<div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"advancedRoomListLogging"} level={SettingLevel.DEVICE} />
</div>
{ betaSection }
{ labsSection }
</div>
);
}

View file

@ -15,15 +15,18 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {Mjolnir} from "../../../../../mjolnir/Mjolnir";
import {ListRule} from "../../../../../mjolnir/ListRule";
import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
import { Mjolnir } from "../../../../../mjolnir/Mjolnir";
import { ListRule } from "../../../../../mjolnir/ListRule";
import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList";
import Modal from "../../../../../Modal";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import AccessibleButton from "../../../elements/AccessibleButton";
import Field from "../../../elements/Field";
interface IState {
busy: boolean;
@ -44,11 +47,11 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
}
private onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value});
this.setState({ newPersonalRule: e.target.value });
};
private onNewListChanged = (e) => {
this.setState({newList: e.target.value});
this.setState({ newList: e.target.value });
};
private onAddPersonalRule = async (e) => {
@ -60,21 +63,20 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
kind = RULE_USER;
}
this.setState({busy: true});
this.setState({ busy: true });
try {
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered
this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, {
title: _t('Error adding ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
this.setState({ busy: false });
}
};
@ -82,63 +84,58 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
e.preventDefault();
e.stopPropagation();
this.setState({busy: true});
this.setState({ busy: true });
try {
const room = await MatrixClientPeg.get().joinRoom(this.state.newList);
await Mjolnir.sharedInstance().subscribeToList(room.roomId);
this.setState({newList: ""}); // this will also cause the new rule to be rendered
this.setState({ newList: "" }); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
title: _t('Error subscribing to list'),
description: _t('Please verify the room ID or address and try again.'),
});
} finally {
this.setState({busy: false});
this.setState({ busy: false });
}
};
private async removePersonalRule(rule: ListRule) {
this.setState({busy: true});
this.setState({ busy: true });
try {
const list = Mjolnir.sharedInstance().getPersonalList();
await list.unbanEntity(rule.kind, rule.entity);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, {
title: _t('Error removing ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
this.setState({ busy: false });
}
}
private async unsubscribeFromList(list: BanList) {
this.setState({busy: true});
this.setState({ busy: true });
try {
await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId);
await MatrixClientPeg.get().leave(list.roomId);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, {
title: _t('Error unsubscribing from list'),
description: _t('Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
this.setState({ busy: false });
}
}
private viewListRules(list: BanList) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? room.name : list.roomId;
@ -153,7 +150,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
};
Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", {roomName: name}),
title: _t("Ban list rules - %(roomName)s", { roomName: name }),
description: (
<div>
<h3>{_t("Server rules")}</h3>
@ -168,8 +165,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
}
private renderPersonalBanListRules() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
@ -199,8 +194,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
}
private renderSubscribedBanLists() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const personalList = Mjolnir.sharedInstance().getPersonalList();
const lists = Mjolnir.sharedInstance().lists.filter(b => {
return personalList? personalList.roomId !== b.roomId : true;
@ -241,8 +234,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const brand = SdkConfig.get().brand;
return (
@ -256,7 +247,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
"Add users and servers you want to ignore here. Use asterisks " +
"to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{ brand }, {code: (s) => <code>{s}</code>},
{ brand }, { code: (s) => <code>{s}</code> },
)}<br />
<br />
{_t(

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import * as sdk from "../../../../../index";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
export default class NotificationUserSettingsTab extends React.Component {

View file

@ -16,14 +16,16 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton";
interface IState {
autoLaunch: boolean;
@ -45,6 +47,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
'breadcrumbs',
];
static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
@ -53,28 +59,32 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
'MessageComposerInput.showStickersButton',
];
static TIMELINE_SETTINGS = [
'showTypingNotifications',
'autoplayGifsAndVideos',
'urlPreviewsEnabled',
'TextualBody.enableBigEmoji',
'showReadReceipts',
static TIME_SETTINGS = [
'showTwelveHourTimestamps',
'alwaysShowTimestamps',
'showRedactions',
];
static CODE_BLOCKS_SETTINGS = [
'enableSyntaxHighlightLanguageDetection',
'expandCodeByDefault',
'scrollToBottomOnMessageSent',
'showCodeLineNumbers',
'showJoinLeaves',
'showAvatarChanges',
'showDisplaynameChanges',
'showImages',
'showChatEffects',
'Pill.shouldShowPillAvatar',
'ctrlFForSearch',
];
static IMAGES_AND_VIDEOS_SETTINGS = [
'urlPreviewsEnabled',
'autoplayGifsAndVideos',
'showImages',
];
static TIMELINE_SETTINGS = [
'showTypingNotifications',
'showRedactions',
'showReadReceipts',
'showJoinLeaves',
'showDisplaynameChanges',
'showChatEffects',
'showAvatarChanges',
'Pill.shouldShowPillAvatar',
'TextualBody.enableBigEmoji',
'scrollToBottomOnMessageSent',
];
static GENERAL_SETTINGS = [
'TagPanel.enableTagPanel',
'promptBeforeInviteUnknownUsers',
@ -143,38 +153,37 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
}
private onAutoLaunchChange = (checked: boolean) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({ autoLaunch: checked }));
};
private onWarnBeforeExitChange = (checked: boolean) => {
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked}));
}
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({ warnBeforeExit: checked }));
};
private onAlwaysShowMenuBarChange = (checked: boolean) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked}));
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({ alwaysShowMenuBar: checked }));
};
private onMinimizeToTrayChange = (checked: boolean) => {
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked}));
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({ minimizeToTray: checked }));
};
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({autocompleteDelay: e.target.value});
this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
};
private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({readMarkerInViewThresholdMs: e.target.value});
this.setState({ readMarkerInViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({readMarkerOutOfViewThresholdMs: e.target.value});
this.setState({ readMarkerOutOfViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
private renderGroup(settingIds: string[]): React.ReactNodeArray {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return settingIds.filter(SettingsStore.isEnabled).map(i => {
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
});
@ -222,11 +231,34 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Keyboard shortcuts")}</span>
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
{ _t("To view all keyboard shortcuts, click here.") }
</AccessibleButton>
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Displaying time")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Code blocks")}</span>
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Images, GIFs and videos")}</span>
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}

View file

@ -17,24 +17,25 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import { sleep } from "matrix-js-sdk/src/utils";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../../Analytics";
import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import {sleep} from "../../../../../utils/promise";
import dis from "../../../../../dispatcher/dispatcher";
import {privateShouldBeEncrypted} from "../../../../../createRoom";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { privateShouldBeEncrypted } from "../../../../../createRoom";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
import CountlyAnalytics from "../../../../../CountlyAnalytics";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -82,12 +83,11 @@ export default class SecurityUserSettingsTab extends React.Component {
this._onAction = this._onAction.bind(this);
}
_onAction({action}) {
_onAction({ action }) {
if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
this.setState({ignoredUserIds, waitingUnignored: newWaitingUnignored});
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
}
}
@ -111,14 +111,14 @@ export default class SecurityUserSettingsTab extends React.Component {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
{ matrixClient: MatrixClientPeg.get() },
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
{ matrixClient: MatrixClientPeg.get() },
);
};
@ -131,13 +131,13 @@ export default class SecurityUserSettingsTab extends React.Component {
}
_onUserUnignored = async (userId) => {
const {ignoredUserIds, waitingUnignored} = this.state;
const { ignoredUserIds, waitingUnignored } = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
const index = currentlyIgnoredUserIds.indexOf(userId);
if (index !== -1) {
currentlyIgnoredUserIds.splice(index, 1);
this.setState(({waitingUnignored}) => ({waitingUnignored: [...waitingUnignored, userId]}));
this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] }));
MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds);
}
};
@ -168,7 +168,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// Accept/reject invite
await action(roomId).then(() => {
// No error, update invited rooms button
this.setState({invitedRoomAmt: self.state.invitedRoomAmt - 1});
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 });
}, async (e) => {
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
@ -253,16 +253,20 @@ export default class SecurityUserSettingsTab extends React.Component {
}
_renderIgnoredUsers() {
const {waitingUnignored, ignoredUserIds} = this.state;
const { waitingUnignored, ignoredUserIds } = this.state;
const userIds = !ignoredUserIds?.length
? _t('You have no ignored users.')
: ignoredUserIds.map((u) => <IgnoredUser
userId={u}
onUnignored={this._onUserUnignored}
key={u}
inProgress={waitingUnignored.includes(u)}
/>);
: ignoredUserIds.map((u) => {
return (
<IgnoredUser
userId={u}
onUnignored={this._onUserUnignored}
key={u}
inProgress={waitingUnignored.includes(u)}
/>
);
});
return (
<div className='mx_SettingsTab_section'>
@ -287,10 +291,10 @@ export default class SecurityUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
<AccessibleButton onClick={onClickAccept} kind='primary' disabled={this.state.managingInvites}>
{_t("Accept all %(invitedRooms)s invites", {invitedRooms: this.state.invitedRoomAmt})}
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
</AccessibleButton>
<AccessibleButton onClick={onClickReject} kind='danger' disabled={this.state.managingInvites}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: this.state.invitedRoomAmt})}
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>

View file

@ -1,234 +0,0 @@
/*
Copyright 2019 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.
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 {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import Modal from "../../../../../Modal";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
export default class VoiceUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
mediaDevices: false,
activeAudioOutput: null,
activeAudioInput: null,
activeVideoInput: null,
};
}
async componentDidMount() {
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this._refreshMediaDevices();
}
}
_refreshMediaDevices = async (stream) => {
this.setState({
mediaDevices: await CallMediaHandler.getDevices(),
activeAudioOutput: CallMediaHandler.getAudioOutput(),
activeAudioInput: CallMediaHandler.getAudioInput(),
activeVideoInput: CallMediaHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
// so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
};
_requestMediaPermissions = async () => {
let constraints;
let stream;
let error;
try {
constraints = {video: true, audio: true};
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
this._refreshMediaDevices(stream);
}
};
_setAudioOutput = (e) => {
CallMediaHandler.setAudioOutput(e.target.value);
this.setState({
activeAudioOutput: e.target.value,
});
};
_setAudioInput = (e) => {
CallMediaHandler.setAudioInput(e.target.value);
this.setState({
activeAudioInput: e.target.value,
});
};
_setVideoInput = (e) => {
CallMediaHandler.setVideoInput(e.target.value);
this.setState({
activeVideoInput: e.target.value,
});
};
_changeWebRtcMethod = (p2p) => {
MatrixClientPeg.get().setForceTURN(!p2p);
};
_changeFallbackICEServerAllowed = (allow) => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
};
_renderDeviceOptions(devices, category) {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
});
}
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
let requestButton = null;
let speakerDropdown = null;
let microphoneDropdown = null;
let webcamDropdown = null;
if (this.state.mediaDevices === false) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
const defaultOption = {
deviceId: '',
label: _t('Default Device'),
};
const getDefaultDevice = (devices) => {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift(defaultOption);
return '';
} else {
return 'default';
}
};
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = (
<Field element="select" label={_t("Audio Output")}
value={this.state.activeAudioOutput || defaultDevice}
onChange={this._setAudioOutput}>
{this._renderDeviceOptions(audioOutputs, 'audioOutput')}
</Field>
);
}
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = (
<Field element="select" label={_t("Microphone")}
value={this.state.activeAudioInput || defaultDevice}
onChange={this._setAudioInput}>
{this._renderDeviceOptions(audioInputs, 'audioInput')}
</Field>
);
}
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = (
<Field element="select" label={_t("Camera")}
value={this.state.activeVideoInput || defaultDevice}
onChange={this._setVideoInput}>
{this._renderDeviceOptions(videoInputs, 'videoInput')}
</Field>
);
}
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section">
{requestButton}
{speakerDropdown}
{microphoneDropdown}
{webcamDropdown}
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this._changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this._changeFallbackICEServerAllowed}
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,206 @@
/*
Copyright 2019 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.
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 { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
import ErrorDialog from '../../../dialogs/ErrorDialog';
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift({ deviceId: '', label: _t('Default Device') });
return '';
} else {
return 'default';
}
};
interface IState extends Record<MediaDeviceKindEnum, string> {
mediaDevices: IMediaDevices;
}
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
mediaDevices: null,
[MediaDeviceKindEnum.AudioOutput]: null,
[MediaDeviceKindEnum.AudioInput]: null,
[MediaDeviceKindEnum.VideoInput]: null,
};
}
async componentDidMount() {
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this.refreshMediaDevices();
}
}
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({
mediaDevices: await MediaDeviceHandler.getDevices(),
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
// so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
};
private requestMediaPermissions = async (): Promise<void> => {
let constraints;
let stream;
let error;
try {
constraints = { video: true, audio: true };
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
this.refreshMediaDevices(stream);
}
};
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
MediaDeviceHandler.instance.setDevice(deviceId, kind);
this.setState<null>({ [kind]: deviceId });
};
private changeWebRtcMethod = (p2p: boolean): void => {
MatrixClientPeg.get().setForceTURN(!p2p);
};
private changeFallbackICEServerAllowed = (allow: boolean): void => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
};
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
});
}
private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
const devices = this.state.mediaDevices[kind].slice(0);
if (devices.length === 0) return null;
const defaultDevice = getDefaultDevice(devices);
return (
<Field
element="select"
label={label}
value={this.state[kind] || defaultDevice}
onChange={(e) => this.setDevice(e.target.value, kind)}
>
{ this.renderDeviceOptions(devices, kind) }
</Field>
);
}
render() {
let requestButton = null;
let speakerDropdown = null;
let microphoneDropdown = null;
let webcamDropdown = null;
if (!this.state.mediaDevices) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
<p>{ _t('No Audio Outputs detected') }</p>
);
microphoneDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
<p>{ _t('No Microphones detected') }</p>
);
webcamDropdown = (
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
<p>{ _t('No Webcams detected') }</p>
);
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section">
{ requestButton }
{ speakerDropdown }
{ microphoneDropdown }
{ webcamDropdown }
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this.changeFallbackICEServerAllowed}
/>
</div>
</div>
);
}
}