Merge remote-tracking branch 'upstream/develop' into feature/copy-version/17603
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
c16827272d
883 changed files with 21771 additions and 16535 deletions
|
@ -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),
|
||||
|
@ -59,7 +59,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
let removeAvatarBtn;
|
||||
if (avatarUrl && removeAvatar) {
|
||||
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -68,13 +68,13 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
{ avatarElement }
|
||||
<div className="mx_AvatarSetting_hover">
|
||||
<div className="mx_AvatarSetting_hoverBg" />
|
||||
<span>{_t("Upload")}</span>
|
||||
<span>{ _t("Upload") }</span>
|
||||
</div>
|
||||
{uploadAvatarBtn}
|
||||
{removeAvatarBtn}
|
||||
{ uploadAvatarBtn }
|
||||
{ removeAvatarBtn }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
@ -91,24 +91,24 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
|
||||
creator = <li>{ _t("This bridge was provisioned by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
}) }</li>;
|
||||
}
|
||||
|
||||
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
|
||||
const bot = <li>{ _t("This bridge is managed by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.bridgebot)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
}) }</li>;
|
||||
|
||||
let networkIcon;
|
||||
|
||||
|
@ -119,9 +119,9 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
name={protocolName}
|
||||
idName={protocolName}
|
||||
url={avatarUrl}
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div className="noProtocolIcon"></div>;
|
||||
|
@ -129,35 +129,37 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
let networkItem = null;
|
||||
if (network) {
|
||||
const networkName = network.displayname || network.id;
|
||||
let networkLink = <span>{networkName}</span>;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
let channelLink = <span>{channelName}</span>;
|
||||
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();
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
{ networkIcon }
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<h3>{ protocolName }</h3>
|
||||
<p className="workspace-channel-details">
|
||||
{networkItem}
|
||||
<span className="channel">{_t("Channel: <channelLink/>", {}, {
|
||||
{ networkItem }
|
||||
<span className="channel">{ _t("Channel: <channelLink/>", {}, {
|
||||
channelLink: () => channelLink,
|
||||
})}</span>
|
||||
}) }</span>
|
||||
</p>
|
||||
<ul className="metadata">
|
||||
{creator} {bot}
|
||||
{ creator } { bot }
|
||||
</ul>
|
||||
</div>
|
||||
</li>);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
@ -99,7 +99,7 @@ export default class ChangePassword extends React.Component {
|
|||
'and re-import them afterwards. ' +
|
||||
'In future this will be improved.',
|
||||
) }
|
||||
{' '}
|
||||
{ ' ' }
|
||||
<a href="https://github.com/vector-im/element-web/issues/2671" target="_blank" rel="noreferrer noopener">
|
||||
https://github.com/vector-im/element-web/issues/2671
|
||||
</a>
|
||||
|
|
|
@ -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"));
|
||||
|
@ -163,29 +163,29 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
let errorSection;
|
||||
if (error) {
|
||||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
errorSection = <div className="error">{ error.toString() }</div>;
|
||||
}
|
||||
|
||||
let summarisedStatus;
|
||||
if (homeserverSupportsCrossSigning === undefined) {
|
||||
summarisedStatus = <Spinner />;
|
||||
} else if (!homeserverSupportsCrossSigning) {
|
||||
summarisedStatus = <p>{_t(
|
||||
summarisedStatus = <p>{ _t(
|
||||
"Your homeserver does not support cross-signing.",
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
} else if (crossSigningReady) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
summarisedStatus = <p>✅ { _t(
|
||||
"Cross-signing is ready for use.",
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
} else if (crossSigningPrivateKeysInStorage) {
|
||||
summarisedStatus = <p>{_t(
|
||||
summarisedStatus = <p>{ _t(
|
||||
"Your account has a cross-signing identity in secret storage, " +
|
||||
"but it is not yet trusted by this session.",
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
} else {
|
||||
summarisedStatus = <p>{_t(
|
||||
summarisedStatus = <p>{ _t(
|
||||
"Cross-signing is not set up.",
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
}
|
||||
|
||||
const keysExistAnywhere = (
|
||||
|
@ -209,7 +209,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
|
||||
{_t("Set up")}
|
||||
{ _t("Set up") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
if (keysExistAnywhere) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
|
||||
{_t("Reset")}
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
@ -225,44 +225,44 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
let actionRow;
|
||||
if (actions.length) {
|
||||
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
{actions}
|
||||
{ actions }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{summarisedStatus}
|
||||
{ summarisedStatus }
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<table className="mx_CrossSigningPanel_statusList"><tbody>
|
||||
<tr>
|
||||
<td>{_t("Cross-signing public keys:")}</td>
|
||||
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
|
||||
<td>{ _t("Cross-signing public keys:") }</td>
|
||||
<td>{ crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Cross-signing private keys:")}</td>
|
||||
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
|
||||
<td>{ _t("Cross-signing private keys:") }</td>
|
||||
<td>{ crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Master private key:")}</td>
|
||||
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<td>{ _t("Master private key:") }</td>
|
||||
<td>{ masterPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Self signing private key:")}</td>
|
||||
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<td>{ _t("Self signing private key:") }</td>
|
||||
<td>{ selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("User signing private key:")}</td>
|
||||
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
<td>{ _t("User signing private key:") }</td>
|
||||
<td>{ userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
<td>{ _t("Homeserver feature support:") }</td>
|
||||
<td>{ homeserverSupportsCrossSigning ? _t("exists") : _t("not found") }</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{actionRow}
|
||||
{ errorSection }
|
||||
{ actionRow }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -16,24 +16,23 @@ 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>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Encryption") }</span>
|
||||
|
||||
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
|
||||
level={SettingLevel.DEVICE}
|
||||
/>
|
||||
<div className="mx_E2eAdvancedPanel_settingLongDescription">{_t(
|
||||
<div className="mx_E2eAdvancedPanel_settingLongDescription">{ _t(
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
||||
)}</div>
|
||||
) }</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,17 +143,16 @@ 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) {
|
||||
eventIndexingSettings = (
|
||||
<div>
|
||||
<div className='mx_SettingsTab_subsectionText'>{_t(
|
||||
<div className='mx_SettingsTab_subsectionText'>{ _t(
|
||||
"Securely cache encrypted messages locally for them " +
|
||||
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
|
||||
{
|
||||
|
@ -163,10 +162,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
count: this.state.roomCount,
|
||||
rooms: formatCountLong(this.state.roomCount),
|
||||
},
|
||||
)}</div>
|
||||
) }</div>
|
||||
<div>
|
||||
<AccessibleButton kind="primary" onClick={this.onManage}>
|
||||
{_t("Manage")}
|
||||
{ _t("Manage") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -174,16 +173,16 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
|
||||
eventIndexingSettings = (
|
||||
<div>
|
||||
<div className='mx_SettingsTab_subsectionText'>{_t(
|
||||
<div className='mx_SettingsTab_subsectionText'>{ _t(
|
||||
"Securely cache encrypted messages locally for them to " +
|
||||
"appear in search results.",
|
||||
)}</div>
|
||||
) }</div>
|
||||
<div>
|
||||
<AccessibleButton kind="primary" disabled={this.state.enabling}
|
||||
onClick={this.onEnable}>
|
||||
{_t("Enable")}
|
||||
{ _t("Enable") }
|
||||
</AccessibleButton>
|
||||
{this.state.enabling ? <InlineSpinner /> : <div />}
|
||||
{ this.state.enabling ? <InlineSpinner /> : <div /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -195,7 +194,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
);
|
||||
|
||||
eventIndexingSettings = (
|
||||
<div className='mx_SettingsTab_subsectionText'>{_t(
|
||||
<div className='mx_SettingsTab_subsectionText'>{ _t(
|
||||
"%(brand)s is missing some components required for securely " +
|
||||
"caching encrypted messages locally. If you'd like to " +
|
||||
"experiment with this feature, build a custom %(brand)s Desktop " +
|
||||
|
@ -206,13 +205,13 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
{
|
||||
nativeLink: sub => <a href={nativeLink}
|
||||
target="_blank" rel="noreferrer noopener"
|
||||
>{sub}</a>,
|
||||
>{ sub }</a>,
|
||||
},
|
||||
)}</div>
|
||||
) }</div>
|
||||
);
|
||||
} else if (!EventIndexPeg.platformHasSupport()) {
|
||||
eventIndexingSettings = (
|
||||
<div className='mx_SettingsTab_subsectionText'>{_t(
|
||||
<div className='mx_SettingsTab_subsectionText'>{ _t(
|
||||
"%(brand)s can't securely cache encrypted messages locally " +
|
||||
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
|
||||
"for encrypted messages to appear in search results.",
|
||||
|
@ -222,32 +221,32 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
{
|
||||
desktopLink: sub => <a href="https://element.io/get-started"
|
||||
target="_blank" rel="noreferrer noopener"
|
||||
>{sub}</a>,
|
||||
>{ sub }</a>,
|
||||
},
|
||||
)}</div>
|
||||
) }</div>
|
||||
);
|
||||
} else {
|
||||
eventIndexingSettings = (
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<p>
|
||||
{this.state.enabling
|
||||
{ this.state.enabling
|
||||
? <InlineSpinner />
|
||||
: _t("Message search initialisation failed")
|
||||
}
|
||||
</p>
|
||||
{EventIndexPeg.error && (
|
||||
{ EventIndexPeg.error && (
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<code>
|
||||
{EventIndexPeg.error.message}
|
||||
{ EventIndexPeg.error.message }
|
||||
</code>
|
||||
<p>
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
|
||||
{_t("Reset")}
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
</details>
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -85,7 +85,7 @@ export default class IntegrationManager extends React.Component {
|
|||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className='mx_IntegrationManager_loading'>
|
||||
<h3>{_t("Connecting to integration manager...")}</h3>
|
||||
<h3>{ _t("Connecting to integration manager...") }</h3>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
@ -94,8 +94,8 @@ export default class IntegrationManager extends React.Component {
|
|||
if (!this.props.connected || this.state.errored) {
|
||||
return (
|
||||
<div className='mx_IntegrationManager_error'>
|
||||
<h3>{_t("Cannot connect to integration manager")}</h3>
|
||||
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
|
||||
<h3>{ _t("Cannot connect to integration manager") }</h3>
|
||||
<p>{ _t("The integration manager is offline or it cannot reach your homeserver.") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,918 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import Modal from '../../../Modal';
|
||||
import {
|
||||
NotificationUtils,
|
||||
VectorPushRulesDefinitions,
|
||||
PushRuleVectorState,
|
||||
ContentRules,
|
||||
} from '../../../notifications';
|
||||
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";
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
// which should be factored out to other files.
|
||||
|
||||
// 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.
|
||||
* These can be removed and forgotten once everyone has moved to the new client.
|
||||
*/
|
||||
const LEGACY_RULES = {
|
||||
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
|
||||
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
|
||||
"im.vector.rule.room_message": ".m.rule.message",
|
||||
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
|
||||
"im.vector.rule.call": ".m.rule.call",
|
||||
"im.vector.rule.notices": ".m.rule.suppress_notices",
|
||||
};
|
||||
|
||||
function portLegacyActions(actions) {
|
||||
const decoded = NotificationUtils.decodeActions(actions);
|
||||
if (decoded !== null) {
|
||||
return NotificationUtils.encodeActions(decoded);
|
||||
} else {
|
||||
// We don't recognise one of the actions here, so we don't try to
|
||||
// canonicalise them.
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.Notifications")
|
||||
export default class Notifications extends React.Component {
|
||||
static phases = {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
||||
ERROR: "ERROR", // There was an error
|
||||
};
|
||||
|
||||
state = {
|
||||
phase: Notifications.phases.LOADING,
|
||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
threepids: [], // used for email notifications
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._refreshFromServer();
|
||||
}
|
||||
|
||||
onEnableNotificationsChange = (checked) => {
|
||||
const self = this;
|
||||
this.setState({
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().setPushRuleEnabled(
|
||||
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
|
||||
).then(function() {
|
||||
self._refreshFromServer();
|
||||
});
|
||||
};
|
||||
|
||||
onEnableDesktopNotificationsChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"notificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
onEnableDesktopNotificationBodyChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"notificationBodyEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
onEnableAudioNotificationsChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"audioNotificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
checked,
|
||||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns the email pusher (pusher of type 'email') for a given
|
||||
* email address. Email pushers all have the same app ID, so since
|
||||
* pushers are unique over (app ID, pushkey), there will be at most
|
||||
* one such pusher.
|
||||
*/
|
||||
getEmailPusher(pushers, address) {
|
||||
if (pushers === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
||||
return pushers[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onEnableEmailNotificationsChange = (address, checked) => {
|
||||
let emailPusherPromise;
|
||||
if (checked) {
|
||||
const data = {};
|
||||
data['brand'] = SdkConfig.get().brand;
|
||||
emailPusherPromise = MatrixClientPeg.get().setPusher({
|
||||
kind: 'email',
|
||||
app_id: 'm.email',
|
||||
pushkey: address,
|
||||
app_display_name: 'Email Notifications',
|
||||
device_display_name: address,
|
||||
lang: navigator.language,
|
||||
data: data,
|
||||
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
|
||||
});
|
||||
} else {
|
||||
const emailPusher = this.getEmailPusher(this.state.pushers, address);
|
||||
emailPusher.kind = null;
|
||||
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
|
||||
}
|
||||
emailPusherPromise.then(() => {
|
||||
this._refreshFromServer();
|
||||
}, (error) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, {
|
||||
title: _t('Error saving email notification preferences'),
|
||||
description: _t('An error occurred whilst saving your email notification preferences.'),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onNotifStateButtonClicked = (event) => {
|
||||
// FIXME: use .bind() rather than className metadata here surely
|
||||
const vectorRuleId = event.target.className.split("-")[0];
|
||||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
|
||||
if ("_keywords" === vectorRuleId) {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
|
||||
} else {
|
||||
const rule = this.getRule(vectorRuleId);
|
||||
if (rule) {
|
||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onKeywordsClicked = (event) => {
|
||||
// Compute the keywords list to display
|
||||
let keywords = [];
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
keywords.push(rule.pattern);
|
||||
}
|
||||
if (keywords.length) {
|
||||
// As keeping the order of per-word push rules hs side is a bit tricky to code,
|
||||
// display the keywords in alphabetical order to the user
|
||||
keywords.sort();
|
||||
|
||||
keywords = keywords.join(", ");
|
||||
} else {
|
||||
keywords = "";
|
||||
}
|
||||
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, {
|
||||
title: _t('Keywords'),
|
||||
description: _t('Enter keywords separated by a comma:'),
|
||||
button: _t('OK'),
|
||||
value: keywords,
|
||||
onFinished: (shouldLeave, newValue) => {
|
||||
if (shouldLeave && newValue !== keywords) {
|
||||
let newKeywords = newValue.split(',');
|
||||
for (const i in newKeywords) {
|
||||
newKeywords[i] = newKeywords[i].trim();
|
||||
}
|
||||
|
||||
// Remove duplicates and empty
|
||||
newKeywords = newKeywords.reduce(function(array, keyword) {
|
||||
if (keyword !== "" && array.indexOf(keyword) < 0) {
|
||||
array.push(keyword);
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
this._setKeywords(newKeywords);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getRule(vectorRuleId) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.vectorRuleId === vectorRuleId) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setPushRuleVectorState(rule, newPushRuleVectorState) {
|
||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
||||
this.setState({
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const deferreds = [];
|
||||
const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
|
||||
|
||||
if (rule.rule) {
|
||||
const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
|
||||
|
||||
if (!actions) {
|
||||
// The new state corresponds to disabling the rule.
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
|
||||
} else {
|
||||
// The new state corresponds to enabling the rule and setting specific actions
|
||||
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(deferreds).then(function() {
|
||||
self._refreshFromServer();
|
||||
}, function(error) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change settings: " + error);
|
||||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
||||
title: _t('Failed to change settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
|
||||
// Is there really a change?
|
||||
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|
||||
|| this.state.vectorContentRules.rules.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
this.setState({
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
// Update all rules in self.state.vectorContentRules
|
||||
const deferreds = [];
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
|
||||
let enabled; let actions;
|
||||
switch (newPushRuleVectorState) {
|
||||
case PushRuleVectorState.ON:
|
||||
if (rule.actions.length !== 1) {
|
||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
|
||||
}
|
||||
|
||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
||||
enabled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case PushRuleVectorState.LOUD:
|
||||
if (rule.actions.length !== 3) {
|
||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
|
||||
}
|
||||
|
||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
||||
enabled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case PushRuleVectorState.OFF:
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (actions) {
|
||||
// Note that the workaround in _updatePushRuleActions will automatically
|
||||
// enable the rule
|
||||
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
|
||||
} else if (enabled != undefined) {
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(deferreds).then(function(resps) {
|
||||
self._refreshFromServer();
|
||||
}, function(error) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Can't update user notification settings: " + error);
|
||||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
||||
title: _t('Can\'t update user notification settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_setKeywords(newKeywords) {
|
||||
this.setState({
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const removeDeferreds = [];
|
||||
|
||||
// Remove per-word push rules of keywords that are no more in the list
|
||||
const vectorContentRulesPatterns = [];
|
||||
for (const i in self.state.vectorContentRules.rules) {
|
||||
const rule = self.state.vectorContentRules.rules[i];
|
||||
|
||||
vectorContentRulesPatterns.push(rule.pattern);
|
||||
|
||||
if (newKeywords.indexOf(rule.pattern) < 0) {
|
||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
||||
}
|
||||
}
|
||||
|
||||
// If the keyword is part of `externalContentRules`, remove the rule
|
||||
// before recreating it in the right Vector path
|
||||
for (const i in self.state.externalContentRules) {
|
||||
const rule = self.state.externalContentRules[i];
|
||||
|
||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
||||
}
|
||||
}
|
||||
|
||||
const onError = function(error) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to update keywords: " + error);
|
||||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
||||
title: _t('Failed to update keywords'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
};
|
||||
|
||||
// Then, add the new ones
|
||||
Promise.all(removeDeferreds).then(function(resps) {
|
||||
const deferreds = [];
|
||||
|
||||
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
|
||||
if (pushRuleVectorStateKind === PushRuleVectorState.OFF) {
|
||||
// When the current global keywords rule is OFF, we need to look at
|
||||
// the flavor of rules in 'vectorContentRules' to apply the same actions
|
||||
// when creating the new rule.
|
||||
// Thus, this new rule will join the 'vectorContentRules' set.
|
||||
if (self.state.vectorContentRules.rules.length) {
|
||||
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(
|
||||
self.state.vectorContentRules.rules[0],
|
||||
);
|
||||
} else {
|
||||
// ON is default
|
||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
||||
}
|
||||
}
|
||||
|
||||
for (const i in newKeywords) {
|
||||
const keyword = newKeywords[i];
|
||||
|
||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
||||
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
|
||||
deferreds.push(cli.addPushRule('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword,
|
||||
}));
|
||||
} else {
|
||||
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(deferreds).then(function(resps) {
|
||||
self._refreshFromServer();
|
||||
}, onError);
|
||||
}, onError);
|
||||
}
|
||||
|
||||
// Create a push rule but disabled
|
||||
_addDisabledPushRule(scope, kind, ruleId, body) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any legacy im.vector rules need to be ported to the new API
|
||||
// for overriding the actions of default rules.
|
||||
_portRulesToNewAPI(rulesets) {
|
||||
const needsUpdate = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
for (const kind in rulesets.global) {
|
||||
const ruleset = rulesets.global[kind];
|
||||
for (let i = 0; i < ruleset.length; ++i) {
|
||||
const rule = ruleset[i];
|
||||
if (rule.rule_id in LEGACY_RULES) {
|
||||
console.log("Porting legacy rule", rule);
|
||||
needsUpdate.push( function(kind, rule) {
|
||||
return cli.setPushRuleActions(
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
|
||||
).then(() =>
|
||||
cli.deletePushRule('global', kind, rule.rule_id),
|
||||
).catch( (e) => {
|
||||
console.warn(`Error when porting legacy rule: ${e}`);
|
||||
});
|
||||
}(kind, rule));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate.length > 0) {
|
||||
// If some of the rules need to be ported then wait for the porting
|
||||
// to happen and then fetch the rules again.
|
||||
return Promise.all(needsUpdate).then(() =>
|
||||
cli.getPushRules(),
|
||||
);
|
||||
} else {
|
||||
// Otherwise return the rules that we already have.
|
||||
return rulesets;
|
||||
}
|
||||
}
|
||||
|
||||
_refreshFromServer = () => {
|
||||
const self = this;
|
||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(
|
||||
self._portRulesToNewAPI,
|
||||
).then(function(rulesets) {
|
||||
/// XXX seriously? wtf is this?
|
||||
MatrixClientPeg.get().pushRules = rulesets;
|
||||
|
||||
// Get homeserver default rules and triage them by categories
|
||||
const ruleCategories = {
|
||||
// The master rule (all notifications disabling)
|
||||
'.m.rule.master': 'master',
|
||||
|
||||
// The default push rules displayed by Vector UI
|
||||
'.m.rule.contains_display_name': 'vector',
|
||||
'.m.rule.contains_user_name': 'vector',
|
||||
'.m.rule.roomnotif': 'vector',
|
||||
'.m.rule.room_one_to_one': 'vector',
|
||||
'.m.rule.encrypted_room_one_to_one': 'vector',
|
||||
'.m.rule.message': 'vector',
|
||||
'.m.rule.encrypted': 'vector',
|
||||
'.m.rule.invite_for_me': 'vector',
|
||||
//'.m.rule.member_event': 'vector',
|
||||
'.m.rule.call': 'vector',
|
||||
'.m.rule.suppress_notices': 'vector',
|
||||
'.m.rule.tombstone': 'vector',
|
||||
|
||||
// Others go to others
|
||||
};
|
||||
|
||||
// HS default rules
|
||||
const defaultRules = {master: [], vector: {}, others: []};
|
||||
|
||||
for (const kind in rulesets.global) {
|
||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
const r = rulesets.global[kind][i];
|
||||
const cat = ruleCategories[r.rule_id];
|
||||
r.kind = kind;
|
||||
|
||||
if (r.rule_id[0] === '.') {
|
||||
if (cat === 'vector') {
|
||||
defaultRules.vector[r.rule_id] = r;
|
||||
} else if (cat === 'master') {
|
||||
defaultRules.master.push(r);
|
||||
} else {
|
||||
defaultRules['others'].push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the master rule if any defined by the hs
|
||||
if (defaultRules.master.length > 0) {
|
||||
self.state.masterPushRule = defaultRules.master[0];
|
||||
}
|
||||
|
||||
// parse the keyword rules into our state
|
||||
const contentRules = ContentRules.parseContentRules(rulesets);
|
||||
self.state.vectorContentRules = {
|
||||
vectorState: contentRules.vectorState,
|
||||
rules: contentRules.rules,
|
||||
};
|
||||
self.state.externalContentRules = contentRules.externalRules;
|
||||
|
||||
// Build the rules displayed in the Vector UI matrix table
|
||||
self.state.vectorPushRules = [];
|
||||
self.state.externalPushRules = [];
|
||||
|
||||
const vectorRuleIds = [
|
||||
'.m.rule.contains_display_name',
|
||||
'.m.rule.contains_user_name',
|
||||
'.m.rule.roomnotif',
|
||||
'_keywords',
|
||||
'.m.rule.room_one_to_one',
|
||||
'.m.rule.encrypted_room_one_to_one',
|
||||
'.m.rule.message',
|
||||
'.m.rule.encrypted',
|
||||
'.m.rule.invite_for_me',
|
||||
//'im.vector.rule.member_event',
|
||||
'.m.rule.call',
|
||||
'.m.rule.suppress_notices',
|
||||
'.m.rule.tombstone',
|
||||
];
|
||||
for (const i in vectorRuleIds) {
|
||||
const vectorRuleId = vectorRuleIds[i];
|
||||
|
||||
if (vectorRuleId === '_keywords') {
|
||||
// keywords needs a special handling
|
||||
// For Vector UI, this is a single global push rule but translated in Matrix,
|
||||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": "_keywords",
|
||||
"description": (
|
||||
<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,
|
||||
});
|
||||
} else {
|
||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
||||
const rule = defaultRules.vector[vectorRuleId];
|
||||
|
||||
const vectorState = ruleDefinition.ruleToVectorState(rule);
|
||||
|
||||
//console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState);
|
||||
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": vectorRuleId,
|
||||
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"rule": rule,
|
||||
"vectorState": vectorState,
|
||||
});
|
||||
|
||||
// if there was a rule which we couldn't parse, add it to the external list
|
||||
if (rule && !vectorState) {
|
||||
rule.description = ruleDefinition.description;
|
||||
self.state.externalPushRules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the rules not managed by Vector UI
|
||||
const otherRulesDescriptions = {
|
||||
'.m.rule.message': _t('Notify for all other messages/rooms'),
|
||||
'.m.rule.fallback': _t('Notify me for anything else'),
|
||||
};
|
||||
|
||||
for (const i in defaultRules.others) {
|
||||
const rule = defaultRules.others[i];
|
||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
||||
|
||||
// Show enabled default rules that was modified by the user
|
||||
if (ruleDescription && rule.enabled && !rule.default) {
|
||||
rule.description = ruleDescription;
|
||||
self.state.externalPushRules.push(rule);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
|
||||
self.setState({pushers: resp.pushers});
|
||||
});
|
||||
|
||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
self.setState({
|
||||
phase: Notifications.phases.DISPLAY,
|
||||
});
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
self.setState({
|
||||
phase: Notifications.phases.ERROR,
|
||||
});
|
||||
}).finally(() => {
|
||||
// actually explicitly update our state having been deep-manipulating it
|
||||
self.setState({
|
||||
masterPushRule: self.state.masterPushRule,
|
||||
vectorContentRules: self.state.vectorContentRules,
|
||||
vectorPushRules: self.state.vectorPushRules,
|
||||
externalContentRules: self.state.externalContentRules,
|
||||
externalPushRules: self.state.externalPushRules,
|
||||
});
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
|
||||
};
|
||||
|
||||
_onClearNotifications = () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
cli.getRooms().forEach(r => {
|
||||
if (r.getUnreadNotificationCount() > 0) {
|
||||
const events = r.getLiveTimeline().getEvents();
|
||||
if (events.length) cli.sendReadReceipt(events.pop());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_updatePushRuleActions(rule, actions, enabled) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return cli.setPushRuleActions(
|
||||
'global', rule.kind, rule.rule_id, actions,
|
||||
).then( function() {
|
||||
// Then, if requested, enabled or disabled the rule
|
||||
if (undefined != enabled) {
|
||||
return cli.setPushRuleEnabled(
|
||||
'global', rule.kind, rule.rule_id, enabled,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
|
||||
return (
|
||||
<tr key={ className }>
|
||||
<th>
|
||||
{ title }
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<input className= {className + "-" + PushRuleVectorState.OFF}
|
||||
type="radio"
|
||||
checked={ pushRuleVectorState === PushRuleVectorState.OFF }
|
||||
onChange={ this.onNotifStateButtonClicked } />
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<input className= {className + "-" + PushRuleVectorState.ON}
|
||||
type="radio"
|
||||
checked={ pushRuleVectorState === PushRuleVectorState.ON }
|
||||
onChange={ this.onNotifStateButtonClicked } />
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<input className= {className + "-" + PushRuleVectorState.LOUD}
|
||||
type="radio"
|
||||
checked={ pushRuleVectorState === PushRuleVectorState.LOUD }
|
||||
onChange={ this.onNotifStateButtonClicked } />
|
||||
</th>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderNotifRulesTableRows() {
|
||||
const rows = [];
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
|
||||
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
|
||||
continue;
|
||||
}
|
||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
hasEmailPusher(pushers, address) {
|
||||
if (pushers === undefined) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
emailNotificationsRow(address, label) {
|
||||
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
|
||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
||||
label={label} key={`emailNotif_${label}`} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
let spinner;
|
||||
if (this.state.phase === Notifications.phases.LOADING) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
let masterPushRuleDiv;
|
||||
if (this.state.masterPushRule) {
|
||||
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
|
||||
onChange={this.onEnableNotificationsChange}
|
||||
label={_t('Enable notifications for this account')} />;
|
||||
}
|
||||
|
||||
let clearNotificationsButton;
|
||||
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
|
||||
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
|
||||
{_t("Clear notifications")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// When enabled, the master rule inhibits all existing rules
|
||||
// So do not show all notification settings
|
||||
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
|
||||
return (
|
||||
<div>
|
||||
{masterPushRuleDiv}
|
||||
|
||||
<div className="mx_UserNotifSettings_notifTable">
|
||||
{ _t('All notifications are currently disabled for all targets.') }
|
||||
</div>
|
||||
|
||||
{clearNotificationsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
|
||||
let emailNotificationsRows;
|
||||
if (emailThreepids.length > 0) {
|
||||
emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow(
|
||||
threePid.address, `${_t('Enable email notifications')} (${threePid.address})`,
|
||||
));
|
||||
} else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) {
|
||||
emailNotificationsRows = <div>
|
||||
{ _t('Add an email address to configure email notifications') }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Build external push rules
|
||||
const externalRules = [];
|
||||
for (const i in this.state.externalPushRules) {
|
||||
const rule = this.state.externalPushRules[i];
|
||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
||||
}
|
||||
|
||||
// Show keywords not displayed by the vector UI as a single external push rule
|
||||
let externalKeywords = [];
|
||||
for (const i in this.state.externalContentRules) {
|
||||
const rule = this.state.externalContentRules[i];
|
||||
externalKeywords.push(rule.pattern);
|
||||
}
|
||||
if (externalKeywords.length) {
|
||||
externalKeywords = externalKeywords.join(", ");
|
||||
externalRules.push(<li>
|
||||
{_t('Notifications on the following keywords follow rules which can’t be displayed here:') }
|
||||
{ externalKeywords }
|
||||
</li>);
|
||||
}
|
||||
|
||||
let devicesSection;
|
||||
if (this.state.pushers === undefined) {
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
|
||||
} else if (this.state.pushers.length === 0) {
|
||||
devicesSection = null;
|
||||
} else {
|
||||
// TODO: It would be great to be able to delete pushers from here too,
|
||||
// and this wouldn't be hard to add.
|
||||
const rows = [];
|
||||
for (let i = 0; i < this.state.pushers.length; ++i) {
|
||||
rows.push(<tr key={ i }>
|
||||
<td>{this.state.pushers[i].app_display_name}</td>
|
||||
<td>{this.state.pushers[i].device_display_name}</td>
|
||||
</tr>);
|
||||
}
|
||||
devicesSection = (<table className="mx_UserNotifSettings_devicesTable">
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>);
|
||||
}
|
||||
if (devicesSection) {
|
||||
devicesSection = (<div>
|
||||
<h3>{ _t('Notification targets') }</h3>
|
||||
{ devicesSection }
|
||||
</div>);
|
||||
}
|
||||
|
||||
let advancedSettings;
|
||||
if (externalRules.length) {
|
||||
const brand = SdkConfig.get().brand;
|
||||
advancedSettings = (
|
||||
<div>
|
||||
<h3>{ _t('Advanced notification settings') }</h3>
|
||||
{ _t('There are advanced notifications which are not shown here.') }<br />
|
||||
{_t(
|
||||
'You might have configured them in a client other than %(brand)s. ' +
|
||||
'You cannot tune them in %(brand)s but they still apply.',
|
||||
{ brand },
|
||||
)}
|
||||
<ul>
|
||||
{ externalRules }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
{masterPushRuleDiv}
|
||||
|
||||
<div className="mx_UserNotifSettings_notifTable">
|
||||
|
||||
{ spinner }
|
||||
|
||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
|
||||
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')} />
|
||||
|
||||
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
|
||||
onChange={this.onEnableAudioNotificationsChange}
|
||||
label={_t('Enable audible notifications for this session')} />
|
||||
|
||||
{ emailNotificationsRows }
|
||||
|
||||
<div className="mx_UserNotifSettings_pushRulesTableWrapper">
|
||||
<table className="mx_UserNotifSettings_pushRulesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="55%"></th>
|
||||
<th width="15%">{ _t('Off') }</th>
|
||||
<th width="15%">{ _t('On') }</th>
|
||||
<th width="15%">{ _t('Noisy') }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{ this.renderNotifRulesTableRows() }
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{ advancedSettings }
|
||||
|
||||
{ devicesSection }
|
||||
|
||||
{ clearNotificationsButton }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
647
src/components/views/settings/Notifications.tsx
Normal file
647
src/components/views/settings/Notifications.tsx
Normal file
|
@ -0,0 +1,647 @@
|
|||
/*
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 Spinner from "../elements/Spinner";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
|
||||
import {
|
||||
ContentRules,
|
||||
IContentRules,
|
||||
PushRuleVectorState,
|
||||
VectorPushRulesDefinitions,
|
||||
VectorState,
|
||||
} from "../../../notifications";
|
||||
import { _t, TranslatedString } from "../../../languageHandler";
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import TagComposer from "../elements/TagComposer";
|
||||
import { objectClone } from "../../../utils/objects";
|
||||
import { arrayDiff } from "../../../utils/arrays";
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
// which should be factored out to other files.
|
||||
|
||||
enum Phase {
|
||||
Loading = "loading",
|
||||
Ready = "ready",
|
||||
Persisting = "persisting", // technically a meta-state for Ready, but whatever
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
enum RuleClass {
|
||||
Master = "master",
|
||||
|
||||
// The vector sections map approximately to UI sections
|
||||
VectorGlobal = "vector_global",
|
||||
VectorMentions = "vector_mentions",
|
||||
VectorOther = "vector_other",
|
||||
Other = "other", // unknown rules, essentially
|
||||
}
|
||||
|
||||
const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component
|
||||
const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions;
|
||||
|
||||
// This array doesn't care about categories: it's just used for a simple sort
|
||||
const RULE_DISPLAY_ORDER: string[] = [
|
||||
// Global
|
||||
RuleId.DM,
|
||||
RuleId.EncryptedDM,
|
||||
RuleId.Message,
|
||||
RuleId.EncryptedMessage,
|
||||
|
||||
// Mentions
|
||||
RuleId.ContainsDisplayName,
|
||||
RuleId.ContainsUserName,
|
||||
RuleId.AtRoomNotification,
|
||||
|
||||
// Other
|
||||
RuleId.InviteToSelf,
|
||||
RuleId.IncomingCall,
|
||||
RuleId.SuppressNotices,
|
||||
RuleId.Tombstone,
|
||||
];
|
||||
|
||||
interface IVectorPushRule {
|
||||
ruleId: RuleId | typeof KEYWORD_RULE_ID | string;
|
||||
rule?: IAnnotatedPushRule;
|
||||
description: TranslatedString | string;
|
||||
vectorState: VectorState;
|
||||
}
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
|
||||
// Optional stuff is required when `phase === Ready`
|
||||
masterPushRule?: IAnnotatedPushRule;
|
||||
vectorKeywordRuleInfo?: IContentRules;
|
||||
vectorPushRules?: {
|
||||
[category in RuleClass]?: IVectorPushRule[];
|
||||
};
|
||||
pushers?: IPusher[];
|
||||
threepids?: IThreepid[];
|
||||
}
|
||||
|
||||
export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
phase: Phase.Loading,
|
||||
};
|
||||
}
|
||||
|
||||
private get isInhibited(): boolean {
|
||||
// Caution: The master rule's enabled state is inverted from expectation. When
|
||||
// the master rule is *enabled* it means all other rules are *disabled* (or
|
||||
// inhibited). Conversely, when the master rule is *disabled* then all other rules
|
||||
// are *enabled* (or operate fine).
|
||||
return this.state.masterPushRule?.enabled;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.refreshFromServer();
|
||||
}
|
||||
|
||||
private async refreshFromServer() {
|
||||
try {
|
||||
const newState = (await Promise.all([
|
||||
this.refreshRules(),
|
||||
this.refreshPushers(),
|
||||
this.refreshThreepids(),
|
||||
])).reduce((p, c) => Object.assign(c, p), {});
|
||||
|
||||
this.setState({
|
||||
...newState,
|
||||
phase: Phase.Ready,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error setting up notifications for settings: ", e);
|
||||
this.setState({ phase: Phase.Error });
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshRules(): Promise<Partial<IState>> {
|
||||
const ruleSets = await MatrixClientPeg.get().getPushRules();
|
||||
|
||||
const categories = {
|
||||
[RuleId.Master]: RuleClass.Master,
|
||||
|
||||
[RuleId.DM]: RuleClass.VectorGlobal,
|
||||
[RuleId.EncryptedDM]: RuleClass.VectorGlobal,
|
||||
[RuleId.Message]: RuleClass.VectorGlobal,
|
||||
[RuleId.EncryptedMessage]: RuleClass.VectorGlobal,
|
||||
|
||||
[RuleId.ContainsDisplayName]: RuleClass.VectorMentions,
|
||||
[RuleId.ContainsUserName]: RuleClass.VectorMentions,
|
||||
[RuleId.AtRoomNotification]: RuleClass.VectorMentions,
|
||||
|
||||
[RuleId.InviteToSelf]: RuleClass.VectorOther,
|
||||
[RuleId.IncomingCall]: RuleClass.VectorOther,
|
||||
[RuleId.SuppressNotices]: RuleClass.VectorOther,
|
||||
[RuleId.Tombstone]: RuleClass.VectorOther,
|
||||
|
||||
// Everything maps to a generic "other" (unknown rule)
|
||||
};
|
||||
|
||||
const defaultRules: {
|
||||
[k in RuleClass]: IAnnotatedPushRule[];
|
||||
} = {
|
||||
[RuleClass.Master]: [],
|
||||
[RuleClass.VectorGlobal]: [],
|
||||
[RuleClass.VectorMentions]: [],
|
||||
[RuleClass.VectorOther]: [],
|
||||
[RuleClass.Other]: [],
|
||||
};
|
||||
|
||||
for (const k in ruleSets.global) {
|
||||
// noinspection JSUnfilteredForInLoop
|
||||
const kind = k as PushRuleKind;
|
||||
for (const r of ruleSets.global[kind]) {
|
||||
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
|
||||
const category = categories[rule.rule_id] ?? RuleClass.Other;
|
||||
|
||||
if (rule.rule_id[0] === '.') {
|
||||
defaultRules[category].push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const preparedNewState: Partial<IState> = {};
|
||||
if (defaultRules.master.length > 0) {
|
||||
preparedNewState.masterPushRule = defaultRules.master[0];
|
||||
} else {
|
||||
// XXX: Can this even happen? How do we safely recover?
|
||||
throw new Error("Failed to locate a master push rule");
|
||||
}
|
||||
|
||||
// Parse keyword rules
|
||||
preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets);
|
||||
|
||||
// Prepare rendering for all of our known rules
|
||||
preparedNewState.vectorPushRules = {};
|
||||
const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther];
|
||||
for (const category of vectorCategories) {
|
||||
preparedNewState.vectorPushRules[category] = [];
|
||||
for (const rule of defaultRules[category]) {
|
||||
const definition = VectorPushRulesDefinitions[rule.rule_id];
|
||||
const vectorState = definition.ruleToVectorState(rule);
|
||||
preparedNewState.vectorPushRules[category].push({
|
||||
ruleId: rule.rule_id,
|
||||
rule, vectorState,
|
||||
description: _t(definition.description),
|
||||
});
|
||||
}
|
||||
|
||||
// Quickly sort the rules for display purposes
|
||||
preparedNewState.vectorPushRules[category].sort((a, b) => {
|
||||
let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId);
|
||||
let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId);
|
||||
|
||||
// Assume unknown things go at the end
|
||||
if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length;
|
||||
if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length;
|
||||
|
||||
return idxA - idxB;
|
||||
});
|
||||
|
||||
if (category === KEYWORD_RULE_CATEGORY) {
|
||||
preparedNewState.vectorPushRules[category].push({
|
||||
ruleId: KEYWORD_RULE_ID,
|
||||
description: _t("Messages containing keywords"),
|
||||
vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return preparedNewState;
|
||||
}
|
||||
|
||||
private refreshPushers(): Promise<Partial<IState>> {
|
||||
return MatrixClientPeg.get().getPushers();
|
||||
}
|
||||
|
||||
private refreshThreepids(): Promise<Partial<IState>> {
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
}
|
||||
|
||||
private showSaveError() {
|
||||
Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, {
|
||||
title: _t('Error saving notification preferences'),
|
||||
description: _t('An error occurred whilst saving your notification preferences.'),
|
||||
});
|
||||
}
|
||||
|
||||
private onMasterRuleChanged = async (checked: boolean) => {
|
||||
this.setState({ phase: Phase.Persisting });
|
||||
|
||||
try {
|
||||
const masterRule = this.state.masterPushRule;
|
||||
await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked);
|
||||
await this.refreshFromServer();
|
||||
} catch (e) {
|
||||
this.setState({ phase: Phase.Error });
|
||||
console.error("Error updating master push rule:", e);
|
||||
this.showSaveError();
|
||||
}
|
||||
};
|
||||
|
||||
private onEmailNotificationsChanged = async (email: string, checked: boolean) => {
|
||||
this.setState({ phase: Phase.Persisting });
|
||||
|
||||
try {
|
||||
if (checked) {
|
||||
await MatrixClientPeg.get().setPusher({
|
||||
kind: "email",
|
||||
app_id: "m.email",
|
||||
pushkey: email,
|
||||
app_display_name: "Email Notifications",
|
||||
device_display_name: email,
|
||||
lang: navigator.language,
|
||||
data: {
|
||||
brand: SdkConfig.get().brand,
|
||||
},
|
||||
|
||||
// We always append for email pushers since we don't want to stop other
|
||||
// accounts notifying to the same email address
|
||||
append: true,
|
||||
});
|
||||
} else {
|
||||
const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email);
|
||||
pusher.kind = null; // flag for delete
|
||||
await MatrixClientPeg.get().setPusher(pusher);
|
||||
}
|
||||
|
||||
await this.refreshFromServer();
|
||||
} catch (e) {
|
||||
this.setState({ phase: Phase.Error });
|
||||
console.error("Error updating email pusher:", e);
|
||||
this.showSaveError();
|
||||
}
|
||||
};
|
||||
|
||||
private onDesktopNotificationsChanged = async (checked: boolean) => {
|
||||
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||
};
|
||||
|
||||
private onDesktopShowBodyChanged = async (checked: boolean) => {
|
||||
await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked);
|
||||
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||
};
|
||||
|
||||
private onAudioNotificationsChanged = async (checked: boolean) => {
|
||||
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||
};
|
||||
|
||||
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => {
|
||||
this.setState({ phase: Phase.Persisting });
|
||||
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (rule.ruleId === KEYWORD_RULE_ID) {
|
||||
// Update all the keywords
|
||||
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
|
||||
let enabled: boolean;
|
||||
let actions: PushRuleAction[];
|
||||
if (checkedState === VectorState.On) {
|
||||
if (rule.actions.length !== 1) { // XXX: Magic number
|
||||
actions = PushRuleVectorState.actionsFor(checkedState);
|
||||
}
|
||||
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
||||
enabled = true;
|
||||
}
|
||||
} else if (checkedState === VectorState.Loud) {
|
||||
if (rule.actions.length !== 3) { // XXX: Magic number
|
||||
actions = PushRuleVectorState.actionsFor(checkedState);
|
||||
}
|
||||
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
||||
enabled = true;
|
||||
}
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
if (actions) {
|
||||
await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions);
|
||||
}
|
||||
if (enabled !== undefined) {
|
||||
await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const definition = VectorPushRulesDefinitions[rule.ruleId];
|
||||
const actions = definition.vectorStateToActions[checkedState];
|
||||
if (!actions) {
|
||||
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false);
|
||||
} else {
|
||||
await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions);
|
||||
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
await this.refreshFromServer();
|
||||
} catch (e) {
|
||||
this.setState({ phase: Phase.Error });
|
||||
console.error("Error updating push rule:", e);
|
||||
this.showSaveError();
|
||||
}
|
||||
};
|
||||
|
||||
private onClearNotificationsClicked = () => {
|
||||
MatrixClientPeg.get().getRooms().forEach(r => {
|
||||
if (r.getUnreadNotificationCount() > 0) {
|
||||
const events = r.getLiveTimeline().getEvents();
|
||||
if (events.length) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
|
||||
try {
|
||||
// De-duplicate and remove empties
|
||||
keywords = Array.from(new Set(keywords)).filter(k => !!k);
|
||||
const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k);
|
||||
|
||||
// Note: Technically because of the UI interaction (at the time of writing), the diff
|
||||
// will only ever be +/-1 so we don't really have to worry about efficiently handling
|
||||
// tons of keyword changes.
|
||||
|
||||
const diff = arrayDiff(oldKeywords, keywords);
|
||||
|
||||
for (const word of diff.removed) {
|
||||
for (const rule of originalRules.filter(r => r.pattern === word)) {
|
||||
await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id);
|
||||
}
|
||||
}
|
||||
|
||||
let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState;
|
||||
if (ruleVectorState === VectorState.Off) {
|
||||
// When the current global keywords rule is OFF, we need to look at
|
||||
// the flavor of existing rules to apply the same actions
|
||||
// when creating the new rule.
|
||||
if (originalRules.length) {
|
||||
ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]);
|
||||
} else {
|
||||
ruleVectorState = VectorState.On; // default
|
||||
}
|
||||
}
|
||||
const kind = PushRuleKind.ContentSpecific;
|
||||
for (const word of diff.added) {
|
||||
await MatrixClientPeg.get().addPushRule('global', kind, word, {
|
||||
actions: PushRuleVectorState.actionsFor(ruleVectorState),
|
||||
pattern: word,
|
||||
});
|
||||
if (ruleVectorState === VectorState.Off) {
|
||||
await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false);
|
||||
}
|
||||
}
|
||||
|
||||
await this.refreshFromServer();
|
||||
} catch (e) {
|
||||
this.setState({ phase: Phase.Error });
|
||||
console.error("Error updating keyword push rules:", e);
|
||||
this.showSaveError();
|
||||
}
|
||||
}
|
||||
|
||||
private onKeywordAdd = (keyword: string) => {
|
||||
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||
|
||||
// We add the keyword immediately as a sort of local echo effect
|
||||
this.setState({
|
||||
phase: Phase.Persisting,
|
||||
vectorKeywordRuleInfo: {
|
||||
...this.state.vectorKeywordRuleInfo,
|
||||
rules: [
|
||||
...this.state.vectorKeywordRuleInfo.rules,
|
||||
|
||||
// XXX: Horrible assumption that we don't need the remaining fields
|
||||
{ pattern: keyword } as IAnnotatedPushRule,
|
||||
],
|
||||
},
|
||||
}, async () => {
|
||||
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
||||
});
|
||||
};
|
||||
|
||||
private onKeywordRemove = (keyword: string) => {
|
||||
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||
|
||||
// We remove the keyword immediately as a sort of local echo effect
|
||||
this.setState({
|
||||
phase: Phase.Persisting,
|
||||
vectorKeywordRuleInfo: {
|
||||
...this.state.vectorKeywordRuleInfo,
|
||||
rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword),
|
||||
},
|
||||
}, async () => {
|
||||
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
||||
});
|
||||
};
|
||||
|
||||
private renderTopSection() {
|
||||
const masterSwitch = <LabelledToggleSwitch
|
||||
value={!this.isInhibited}
|
||||
label={_t("Enable for this account")}
|
||||
onChange={this.onMasterRuleChanged}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>;
|
||||
|
||||
// If all the rules are inhibited, don't show anything.
|
||||
if (this.isInhibited) {
|
||||
return masterSwitch;
|
||||
}
|
||||
|
||||
const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email)
|
||||
.map(e => <LabelledToggleSwitch
|
||||
key={e.address}
|
||||
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
|
||||
label={_t("Enable email notifications for %(email)s", { email: e.address })}
|
||||
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>);
|
||||
|
||||
return <>
|
||||
{ masterSwitch }
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={SettingsStore.getValue("notificationsEnabled")}
|
||||
onChange={this.onDesktopNotificationsChanged}
|
||||
label={_t('Enable desktop notifications for this session')}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={SettingsStore.getValue("notificationBodyEnabled")}
|
||||
onChange={this.onDesktopShowBodyChanged}
|
||||
label={_t('Show message in desktop notification')}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>
|
||||
|
||||
<LabelledToggleSwitch
|
||||
value={SettingsStore.getValue("audioNotificationsEnabled")}
|
||||
onChange={this.onAudioNotificationsChanged}
|
||||
label={_t('Enable audible notifications for this session')}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>
|
||||
|
||||
{ emailSwitches }
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderCategory(category: RuleClass) {
|
||||
if (category !== RuleClass.VectorOther && this.isInhibited) {
|
||||
return null; // nothing to show for the section
|
||||
}
|
||||
|
||||
let clearNotifsButton: JSX.Element;
|
||||
if (
|
||||
category === RuleClass.VectorOther
|
||||
&& MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)
|
||||
) {
|
||||
clearNotifsButton = <AccessibleButton
|
||||
onClick={this.onClearNotificationsClicked}
|
||||
kind='danger'
|
||||
className='mx_UserNotifSettings_clearNotifsButton'
|
||||
>{ _t("Clear notifications") }</AccessibleButton>;
|
||||
}
|
||||
|
||||
if (category === RuleClass.VectorOther && this.isInhibited) {
|
||||
// only render the utility buttons (if needed)
|
||||
if (clearNotifsButton) {
|
||||
return <div className='mx_UserNotifSettings_floatingSection'>
|
||||
<div>{ _t("Other") }</div>
|
||||
{ clearNotifsButton }
|
||||
</div>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let keywordComposer: JSX.Element;
|
||||
if (category === RuleClass.VectorMentions) {
|
||||
keywordComposer = <TagComposer
|
||||
tags={this.state.vectorKeywordRuleInfo?.rules.map(r => r.pattern)}
|
||||
onAdd={this.onKeywordAdd}
|
||||
onRemove={this.onKeywordRemove}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
label={_t("Keyword")}
|
||||
placeholder={_t("New keyword")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const makeRadio = (r: IVectorPushRule, s: VectorState) => (
|
||||
<StyledRadioButton
|
||||
key={r.ruleId}
|
||||
name={r.ruleId}
|
||||
checked={r.vectorState === s}
|
||||
onChange={this.onRadioChecked.bind(this, r, s)}
|
||||
disabled={this.state.phase === Phase.Persisting}
|
||||
/>
|
||||
);
|
||||
|
||||
const rows = this.state.vectorPushRules[category].map(r => <tr key={category + r.ruleId}>
|
||||
<td>{ r.description }</td>
|
||||
<td>{ makeRadio(r, VectorState.Off) }</td>
|
||||
<td>{ makeRadio(r, VectorState.On) }</td>
|
||||
<td>{ makeRadio(r, VectorState.Loud) }</td>
|
||||
</tr>);
|
||||
|
||||
let sectionName: TranslatedString;
|
||||
switch (category) {
|
||||
case RuleClass.VectorGlobal:
|
||||
sectionName = _t("Global");
|
||||
break;
|
||||
case RuleClass.VectorMentions:
|
||||
sectionName = _t("Mentions & keywords");
|
||||
break;
|
||||
case RuleClass.VectorOther:
|
||||
sectionName = _t("Other");
|
||||
break;
|
||||
default:
|
||||
throw new Error("Developer error: Unnamed notifications section: " + category);
|
||||
}
|
||||
|
||||
return <>
|
||||
<table className='mx_UserNotifSettings_pushRulesTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{ sectionName }</th>
|
||||
<th>{ _t("Off") }</th>
|
||||
<th>{ _t("On") }</th>
|
||||
<th>{ _t("Noisy") }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
{ clearNotifsButton }
|
||||
{ keywordComposer }
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderTargets() {
|
||||
if (this.isInhibited) return null; // no targets if there's no notifications
|
||||
|
||||
const rows = this.state.pushers.map(p => <tr key={p.kind+p.pushkey}>
|
||||
<td>{ p.app_display_name }</td>
|
||||
<td>{ p.device_display_name }</td>
|
||||
</tr>);
|
||||
|
||||
if (!rows.length) return null; // no targets to show
|
||||
|
||||
return <div className='mx_UserNotifSettings_floatingSection'>
|
||||
<div>{ _t("Notification targets") }</div>
|
||||
<table>
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.phase === Phase.Loading) {
|
||||
// Ends up default centered
|
||||
return <Spinner />;
|
||||
} else if (this.state.phase === Phase.Error) {
|
||||
return <p>{ _t("There was an error loading your notification settings.") }</p>;
|
||||
}
|
||||
|
||||
return <div className='mx_UserNotifSettings'>
|
||||
{ this.renderTopSection() }
|
||||
{ this.renderCategory(RuleClass.VectorGlobal) }
|
||||
{ this.renderCategory(RuleClass.VectorMentions) }
|
||||
{ this.renderCategory(RuleClass.VectorOther) }
|
||||
{ this.renderTargets() }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -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 = {};
|
||||
|
@ -149,12 +149,12 @@ export default class ProfileSettings extends React.Component {
|
|||
let hostingSignup = null;
|
||||
if (hostingSignupLink) {
|
||||
hostingSignup = <span className="mx_ProfileSettings_hostingSignup">
|
||||
{_t(
|
||||
{ _t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
|
@ -178,7 +178,7 @@ export default class ProfileSettings extends React.Component {
|
|||
/>
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Profile") }</span>
|
||||
<Field
|
||||
label={_t("Display Name")}
|
||||
type="text" value={this.state.displayName}
|
||||
|
@ -186,8 +186,8 @@ export default class ProfileSettings extends React.Component {
|
|||
onChange={this._onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{this.state.userId}
|
||||
{hostingSignup}
|
||||
{ this.state.userId }
|
||||
{ hostingSignup }
|
||||
</p>
|
||||
</div>
|
||||
<AvatarSetting
|
||||
|
@ -203,14 +203,14 @@ export default class ProfileSettings extends React.Component {
|
|||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -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,7 +131,7 @@ 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();
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -221,7 +221,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
if (error) {
|
||||
statusDescription = (
|
||||
<div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
{ _t("Unable to load key backup status") }
|
||||
</div>
|
||||
);
|
||||
} else if (loading) {
|
||||
|
@ -230,19 +230,19 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
let restoreButtonCaption = _t("Restore from Backup");
|
||||
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
statusDescription = <p>✅ {_t("This session is backing up your keys. ")}</p>;
|
||||
statusDescription = <p>✅ { _t("This session is backing up your keys. ") }</p>;
|
||||
} else {
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"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>},
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
{ b: sub => <b>{ sub }</b> },
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Connect this session to key backup before signing out to avoid " +
|
||||
"losing any keys that may only be on this session.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
</>;
|
||||
restoreButtonCaption = _t("Connect this session to Key Backup");
|
||||
}
|
||||
|
@ -253,11 +253,11 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
uploadStatus = "";
|
||||
} else if (sessionsRemaining > 0) {
|
||||
uploadStatus = <div>
|
||||
{_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
|
||||
{ _t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining }) } <br />
|
||||
</div>;
|
||||
} else {
|
||||
uploadStatus = <div>
|
||||
{_t("All keys backed up")} <br />
|
||||
{ _t("All keys backed up") } <br />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -265,13 +265,13 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||
const validity = sub =>
|
||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||
{sub}
|
||||
{ sub }
|
||||
</span>;
|
||||
const verify = sub =>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
{ sub }
|
||||
</span>;
|
||||
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
|
||||
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{ deviceName }</span>;
|
||||
const fromThisDevice = (
|
||||
sig.device &&
|
||||
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
|
||||
|
@ -339,7 +339,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
return <div key={i}>
|
||||
{sigStatus}
|
||||
{ sigStatus }
|
||||
</div>;
|
||||
});
|
||||
if (backupSigStatus.sigs.length === 0) {
|
||||
|
@ -353,45 +353,45 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
|
||||
extraDetailsTableRows = <>
|
||||
<tr>
|
||||
<td>{_t("Backup version:")}</td>
|
||||
<td>{backupInfo.version}</td>
|
||||
<td>{ _t("Backup version:") }</td>
|
||||
<td>{ backupInfo.version }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Algorithm:")}</td>
|
||||
<td>{backupInfo.algorithm}</td>
|
||||
<td>{ _t("Algorithm:") }</td>
|
||||
<td>{ backupInfo.algorithm }</td>
|
||||
</tr>
|
||||
</>;
|
||||
|
||||
extraDetails = <>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
{ uploadStatus }
|
||||
<div>{ backupSigStatuses }</div>
|
||||
<div>{ trustedLocally }</div>
|
||||
</>;
|
||||
|
||||
actions.push(
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
{ restoreButtonCaption }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
if (!isSecureBackupRequired()) {
|
||||
actions.push(
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
{ _t("Delete Backup") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Your keys are <b>not being backed up from this session</b>.", {},
|
||||
{b: sub => <b>{sub}</b>},
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
{ b: sub => <b>{ sub }</b> },
|
||||
) }</p>
|
||||
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
||||
</>;
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Set up")}
|
||||
{ _t("Set up") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
@ -399,7 +399,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
if (secretStorageKeyInAccount) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
||||
{_t("Reset")}
|
||||
{ _t("Reset") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
@ -417,47 +417,47 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
let actionRow;
|
||||
if (actions.length) {
|
||||
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
|
||||
{actions}
|
||||
{ actions }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Back up your encryption keys with your account data in case you " +
|
||||
"lose access to your sessions. Your keys will be secured with a " +
|
||||
"unique Security Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
) }</p>
|
||||
{ statusDescription }
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<table className="mx_SecureBackupPanel_statusList"><tbody>
|
||||
<tr>
|
||||
<td>{_t("Backup key stored:")}</td>
|
||||
<td>{ _t("Backup key stored:") }</td>
|
||||
<td>{
|
||||
backupKeyStored === true ? _t("in secret storage") : _t("not stored")
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Backup key cached:")}</td>
|
||||
<td>{ _t("Backup key cached:") }</td>
|
||||
<td>
|
||||
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{backupKeyWellFormedText}
|
||||
{ backupKeyCached ? _t("cached locally") : _t("not found locally") }
|
||||
{ backupKeyWellFormedText }
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
<td>{ _t("Secret storage public key:") }</td>
|
||||
<td>{ secretStorageKeyInAccount ? _t("in account data") : _t("not found") }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage:")}</td>
|
||||
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
|
||||
<td>{ _t("Secret storage:") }</td>
|
||||
<td>{ secretStorageReady ? _t("ready") : _t("not ready") }</td>
|
||||
</tr>
|
||||
{extraDetailsTableRows}
|
||||
{ extraDetailsTableRows }
|
||||
</tbody></table>
|
||||
{extraDetails}
|
||||
{ extraDetails }
|
||||
</details>
|
||||
{actionRow}
|
||||
{ actionRow }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -41,7 +44,7 @@ const REACHABILITY_TIMEOUT = 10000; // ms
|
|||
async function checkIdentityServerUrl(u) {
|
||||
const parsedUrl = url.parse(u);
|
||||
|
||||
if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS");
|
||||
if (parsedUrl.protocol !== 'https:') return _t("Identity server URL must be HTTPS");
|
||||
|
||||
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
|
||||
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
|
||||
|
@ -50,17 +53,17 @@ 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");
|
||||
return _t("Could not connect to identity server");
|
||||
}
|
||||
} catch (e) {
|
||||
return _t("Could not connect to Identity Server");
|
||||
return _t("Could not connect to identity server");
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// Whether or not the ID server is missing terms. This affects the text
|
||||
// Whether or not the identity server is missing terms. This affects the text
|
||||
// shown to the user.
|
||||
missingTerms: boolean;
|
||||
}
|
||||
|
@ -84,7 +87,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
|
||||
let defaultIdServer = '';
|
||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
|
||||
// If no ID server is configured but there's one in the config, prepopulate
|
||||
// If no identity server is configured but there's one in the config, prepopulate
|
||||
// the field to help the user.
|
||||
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
|
||||
}
|
||||
|
@ -109,7 +112,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
// We react to changes in the ID server in the event the user is staring at this form
|
||||
// We react to changes in the identity server in the event the user is staring at this form
|
||||
// when changing their identity server on another device.
|
||||
if (payload.action !== "id_server_changed") return;
|
||||
|
||||
|
@ -121,18 +124,17 @@ 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") }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
return <span className='warning'>{this.state.error}</span>;
|
||||
return <span className='warning'>{ this.state.error }</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -191,8 +193,8 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
"Disconnect from the identity server <current /> and " +
|
||||
"connect to <new /> instead?", {},
|
||||
{
|
||||
current: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
|
||||
new: sub => <b>{abbreviateUrl(idServer)}</b>,
|
||||
current: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
|
||||
new: sub => <b>{ abbreviateUrl(idServer) }</b>,
|
||||
},
|
||||
),
|
||||
button: _t("Continue"),
|
||||
|
@ -217,16 +219,15 @@ 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: (
|
||||
<div>
|
||||
<span className="warning">
|
||||
{_t("The identity server you have chosen does not have any terms of service.")}
|
||||
{ _t("The identity server you have chosen does not have any terms of service.") }
|
||||
</span>
|
||||
<span>
|
||||
{_t("Only continue if you trust the owner of the server.")}
|
||||
{ _t("Only continue if you trust the owner of the server.") }
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -277,41 +278,41 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
let message;
|
||||
let danger = false;
|
||||
const messageElements = {
|
||||
idserver: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
|
||||
b: sub => <b>{sub}</b>,
|
||||
idserver: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
};
|
||||
if (!currentServerReachable) {
|
||||
message = <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"You should <b>remove your personal data</b> from identity server " +
|
||||
"<idserver /> before disconnecting. Unfortunately, identity server " +
|
||||
"<idserver /> is currently offline or cannot be reached.",
|
||||
{}, messageElements,
|
||||
)}</p>
|
||||
<p>{_t("You should:")}</p>
|
||||
) }</p>
|
||||
<p>{ _t("You should:") }</p>
|
||||
<ul>
|
||||
<li>{_t(
|
||||
<li>{ _t(
|
||||
"check your browser plugins for anything that might block " +
|
||||
"the identity server (such as Privacy Badger)",
|
||||
)}</li>
|
||||
<li>{_t("contact the administrators of identity server <idserver />", {}, {
|
||||
) }</li>
|
||||
<li>{ _t("contact the administrators of identity server <idserver />", {}, {
|
||||
idserver: messageElements.idserver,
|
||||
})}</li>
|
||||
<li>{_t("wait and try again later")}</li>
|
||||
}) }</li>
|
||||
<li>{ _t("wait and try again later") }</li>
|
||||
</ul>
|
||||
</div>;
|
||||
danger = true;
|
||||
button = _t("Disconnect anyway");
|
||||
} else if (boundThreepids.length) {
|
||||
message = <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"You are still <b>sharing your personal data</b> on the identity " +
|
||||
"server <idserver />.", {}, messageElements,
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"We recommend that you remove your email addresses and phone numbers " +
|
||||
"from the identity server before disconnecting.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
</div>;
|
||||
danger = true;
|
||||
button = _t("Disconnect anyway");
|
||||
|
@ -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,28 +352,26 @@ 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;
|
||||
if (idServerUrl) {
|
||||
sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
|
||||
sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
|
||||
bodyText = _t(
|
||||
"You are currently using <server></server> to discover and be discoverable by " +
|
||||
"existing contacts you know. You can change your identity server below.",
|
||||
{},
|
||||
{ server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
|
||||
{ server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
|
||||
);
|
||||
if (this.props.missingTerms) {
|
||||
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 {
|
||||
sectionTitle = _t("Identity Server");
|
||||
sectionTitle = _t("Identity server");
|
||||
bodyText = _t(
|
||||
"You are not currently using an identity server. " +
|
||||
"To discover and be discoverable by existing contacts you know, " +
|
||||
|
@ -398,13 +396,12 @@ 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>
|
||||
<span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
|
||||
<span className="mx_SettingsTab_subsectionText">{ discoBodyText }</span>
|
||||
<AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
|
||||
{discoButtonContent}
|
||||
{ discoButtonContent }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
@ -412,10 +409,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this.checkIdServer}>
|
||||
<span className="mx_SettingsTab_subheading">
|
||||
{sectionTitle}
|
||||
{ sectionTitle }
|
||||
</span>
|
||||
<span className="mx_SettingsTab_subsectionText">
|
||||
{bodyText}
|
||||
{ bodyText }
|
||||
</span>
|
||||
<Field
|
||||
label={_t("Enter a new identity server")}
|
||||
|
@ -432,8 +429,8 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
<AccessibleButton type="submit" kind="primary_sm"
|
||||
onClick={this.checkIdServer}
|
||||
disabled={!this.idServerChangeEnabled()}
|
||||
>{_t("Change")}</AccessibleButton>
|
||||
{discoSection}
|
||||
>{ _t("Change") }</AccessibleButton>
|
||||
{ discoSection }
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -55,30 +65,30 @@ export default class SetIntegrationManager extends React.Component {
|
|||
if (currentManager) {
|
||||
managerName = `(${currentManager.name})`;
|
||||
bodyText = _t(
|
||||
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
|
||||
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
|
||||
"and sticker packs.",
|
||||
{serverName: currentManager.name},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
{ serverName: currentManager.name },
|
||||
{ b: sub => <b>{ sub }</b> },
|
||||
);
|
||||
} else {
|
||||
bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
|
||||
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_SetIntegrationManager'>
|
||||
<div className="mx_SettingsTab_heading">
|
||||
<span>{_t("Manage integrations")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{managerName}</span>
|
||||
<span>{ _t("Manage integrations") }</span>
|
||||
<span className="mx_SettingsTab_subheading">{ managerName }</span>
|
||||
<ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
|
||||
</div>
|
||||
<span className="mx_SettingsTab_subsectionText">
|
||||
{bodyText}
|
||||
{ bodyText }
|
||||
<br />
|
||||
<br />
|
||||
{_t(
|
||||
"Integration Managers receive configuration data, and can modify widgets, " +
|
||||
{ _t(
|
||||
"Integration managers receive configuration data, and can modify widgets, " +
|
||||
"send room invites, and set power levels on your behalf.",
|
||||
)}
|
||||
) }
|
||||
</span>
|
||||
</div>
|
||||
);
|
|
@ -17,25 +17,25 @@ 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> {
|
||||
_onRemove = (e) => {
|
||||
private onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -45,9 +45,9 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
|
|||
render() {
|
||||
return (
|
||||
<div className="mx_ExistingSpellCheckLanguage">
|
||||
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
|
||||
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -60,15 +60,15 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
|
|||
super(props);
|
||||
this.state = {
|
||||
newLanguage: "",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_onRemoved = (language) => {
|
||||
private onRemoved = (language: string) => {
|
||||
const languages = this.props.languages.filter((e) => e !== language);
|
||||
this.props.onLanguagesChange(languages);
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
private onAddClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -77,35 +77,35 @@ 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) => {
|
||||
private onNewLanguageChange = (language: string) => {
|
||||
if (this.state.newLanguage === language) return;
|
||||
this.setState({newLanguage: language});
|
||||
this.setState({ newLanguage: language });
|
||||
};
|
||||
|
||||
render() {
|
||||
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
||||
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
||||
return <ExistingSpellCheckLanguage language={e} onRemoved={this.onRemoved} key={e} />;
|
||||
});
|
||||
|
||||
const addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SpellCheckLanguages">
|
||||
{existingSpellCheckLanguages}
|
||||
<form onSubmit={this._onAddClick} noValidate={true}>
|
||||
{ existingSpellCheckLanguages }
|
||||
<form onSubmit={this.onAddClick} noValidate={true}>
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value={this.state.newLanguage}
|
||||
onOptionChange={this._onNewLanguageChange} />
|
||||
{addButton}
|
||||
onOptionChange={this.onNewLanguageChange} />
|
||||
{ addButton }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
@ -42,7 +42,7 @@ function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
|
|||
return _t('Downloading update...');
|
||||
case UpdateCheckStatus.Ready:
|
||||
return _t("New version available. <a>Update now.</a>", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>,
|
||||
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -72,14 +72,14 @@ const UpdateCheckButton = () => {
|
|||
let suffix;
|
||||
if (state) {
|
||||
suffix = <span className="mx_UpdateCheckButton_summary">
|
||||
{getStatusText(state.status, state.detail)}
|
||||
{busy && <InlineSpinner />}
|
||||
{ getStatusText(state.status, state.detail) }
|
||||
{ busy && <InlineSpinner /> }
|
||||
</span>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
|
||||
{_t("Check for update")}
|
||||
{ _t("Check for update") }
|
||||
</AccessibleButton>
|
||||
{ suffix }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -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,21 +88,21 @@ 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"
|
||||
>
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -110,9 +110,9 @@ export class ExistingEmailAddress extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
|
||||
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -167,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")),
|
||||
|
@ -185,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) {
|
||||
|
@ -204,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, {
|
||||
|
@ -229,19 +229,19 @@ export default class EmailAddresses extends React.Component {
|
|||
|
||||
let addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
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>
|
||||
<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")}
|
||||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -249,7 +249,7 @@ export default class EmailAddresses extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_EmailAddresses">
|
||||
{existingEmailElements}
|
||||
{ existingEmailElements }
|
||||
<form
|
||||
onSubmit={this._onAddClick}
|
||||
autoComplete="off"
|
||||
|
@ -264,7 +264,7 @@ export default class EmailAddresses extends React.Component {
|
|||
value={this.state.newEmailAddress}
|
||||
onChange={this._onChangeNewEmailAddress}
|
||||
/>
|
||||
{addButton}
|
||||
{ addButton }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,21 +83,21 @@ 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"
|
||||
>
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -105,9 +105,9 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
|
||||
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -164,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")),
|
||||
|
@ -182,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]) => {
|
||||
|
@ -205,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);
|
||||
|
@ -214,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() {
|
||||
|
@ -230,7 +230,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
|
||||
let addVerifySection = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
if (this.state.verifying) {
|
||||
|
@ -238,10 +238,10 @@ export default class PhoneNumbers extends React.Component {
|
|||
addVerifySection = (
|
||||
<div>
|
||||
<div>
|
||||
{_t("A text message has been sent to +%(msisdn)s. " +
|
||||
"Please enter the verification code it contains.", { msisdn: msisdn })}
|
||||
{ _t("A text message has been sent to +%(msisdn)s. " +
|
||||
"Please enter the verification code it contains.", { msisdn: msisdn }) }
|
||||
<br />
|
||||
{this.state.verifyError}
|
||||
{ this.state.verifyError }
|
||||
</div>
|
||||
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<Field
|
||||
|
@ -257,7 +257,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
{_t("Continue")}
|
||||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -274,7 +274,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_PhoneNumbers">
|
||||
{existingPhoneElements}
|
||||
{ existingPhoneElements }
|
||||
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
||||
<div className="mx_PhoneNumbers_input">
|
||||
<Field
|
||||
|
@ -288,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
</form>
|
||||
{addVerifySection}
|
||||
{ addVerifySection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -198,14 +198,14 @@ export class EmailAddress extends React.Component {
|
|||
let status;
|
||||
if (verifying) {
|
||||
status = <span>
|
||||
{_t("Verify the link in your inbox")}
|
||||
{ _t("Verify the link in your inbox") }
|
||||
<AccessibleButton
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
kind="primary_sm"
|
||||
onClick={this.onContinueClick}
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
{_t("Complete")}
|
||||
{ _t("Complete") }
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
} else if (bound) {
|
||||
|
@ -214,7 +214,7 @@ export class EmailAddress extends React.Component {
|
|||
kind="danger_sm"
|
||||
onClick={this.onRevokeClick}
|
||||
>
|
||||
{_t("Revoke")}
|
||||
{ _t("Revoke") }
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
status = <AccessibleButton
|
||||
|
@ -222,14 +222,14 @@ export class EmailAddress extends React.Component {
|
|||
kind="primary_sm"
|
||||
onClick={this.onShareClick}
|
||||
>
|
||||
{_t("Share")}
|
||||
{ _t("Share") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
<span className="mx_ExistingEmailAddress_email">{address}</span>
|
||||
{status}
|
||||
<span className="mx_ExistingEmailAddress_email">{ address }</span>
|
||||
{ status }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -249,13 +249,13 @@ export default class EmailAddresses extends React.Component {
|
|||
});
|
||||
} else {
|
||||
content = <span className="mx_SettingsTab_subsectionText">
|
||||
{_t("Discovery options will appear once you have added an email above.")}
|
||||
{ _t("Discovery options will appear once you have added an email above.") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EmailAddresses">
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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") });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,9 +205,9 @@ export class PhoneNumber extends React.Component {
|
|||
if (verifying) {
|
||||
status = <span className="mx_ExistingPhoneNumber_verification">
|
||||
<span>
|
||||
{_t("Please enter verification code sent via text.")}
|
||||
{ _t("Please enter verification code sent via text.") }
|
||||
<br />
|
||||
{this.state.verifyError}
|
||||
{ this.state.verifyError }
|
||||
</span>
|
||||
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
||||
<Field
|
||||
|
@ -226,7 +226,7 @@ export class PhoneNumber extends React.Component {
|
|||
kind="danger_sm"
|
||||
onClick={this.onRevokeClick}
|
||||
>
|
||||
{_t("Revoke")}
|
||||
{ _t("Revoke") }
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
status = <AccessibleButton
|
||||
|
@ -234,14 +234,14 @@ export class PhoneNumber extends React.Component {
|
|||
kind="primary_sm"
|
||||
onClick={this.onShareClick}
|
||||
>
|
||||
{_t("Share")}
|
||||
{ _t("Share") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
<span className="mx_ExistingPhoneNumber_address">+{address}</span>
|
||||
{status}
|
||||
<span className="mx_ExistingPhoneNumber_address">+{ address }</span>
|
||||
{ status }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -261,13 +261,13 @@ export default class PhoneNumbers extends React.Component {
|
|||
});
|
||||
} else {
|
||||
content = <span className="mx_SettingsTab_subsectionText">
|
||||
{_t("Discovery options will appear once you have added a phone number above.")}
|
||||
{ _t("Discovery options will appear once you have added a phone number above.") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_PhoneNumbers">
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,59 +111,61 @@ 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.",
|
||||
{}, {
|
||||
"b": (sub) => <b>{sub}</b>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
"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>
|
||||
{this.props.roomId}
|
||||
<span>{ _t("Internal room ID:") }</span>
|
||||
{ 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>
|
||||
{room.getVersion()}
|
||||
<span>{ _t("Room version:") }</span>
|
||||
{ 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>
|
|
@ -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",
|
||||
|
@ -61,36 +61,36 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
|
|||
let content: JSX.Element;
|
||||
if (bridgeEvents.length > 0) {
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"This room is bridging messages to the following platforms. " +
|
||||
"<a>Learn more.</a>", {},
|
||||
{
|
||||
// TODO: We don't have this link yet: this will prevent the translators
|
||||
// having to re-translate the string when we do.
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
|
||||
},
|
||||
)}</p>
|
||||
) }</p>
|
||||
<ul className="mx_RoomSettingsDialog_BridgeList">
|
||||
{ bridgeEvents.map((event) => this.renderBridgeCard(event, room)) }
|
||||
</ul>
|
||||
</div>;
|
||||
} else {
|
||||
content = <p>{_t(
|
||||
content = <p>{ _t(
|
||||
"This room isn’t bridging messages to any platforms. " +
|
||||
"<a>Learn more.</a>", {},
|
||||
{
|
||||
// TODO: We don't have this link yet: this will prevent the translators
|
||||
// having to re-translate the string when we do.
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
|
||||
},
|
||||
)}</p>;
|
||||
) }</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Bridges")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Bridges") }</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,13 +60,12 @@ 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", "");
|
||||
|
||||
let urlPreviewSettings = <>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("URL Previews") }</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</div>
|
||||
|
@ -78,7 +77,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
let flairSection;
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
flairSection = <>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Flair") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings
|
||||
roomId={room.roomId}
|
||||
|
@ -91,22 +90,22 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("General")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
<div className='mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection'>
|
||||
<RoomProfileSettings roomId={this.props.roomId} />
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
|
||||
<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} />
|
||||
canonicalAliasEvent={canonicalAliasEv} />
|
||||
</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
|
||||
{ flairSection }
|
||||
{ urlPreviewSettings }
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
|
|
|
@ -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) {
|
||||
|
@ -142,36 +142,36 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
if (this.state.uploadedFile) {
|
||||
currentUploadedFile = (
|
||||
<div>
|
||||
<span>{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code></span>
|
||||
<span>{ _t("Uploaded sound") }: <code>{ this.state.uploadedFile.name }</code></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Sounds")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
||||
<div>
|
||||
<span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
|
||||
<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>
|
||||
<h3>{_t("Set a new custom sound")}</h3>
|
||||
<h3>{ _t("Set a new custom sound") }</h3>
|
||||
<form autoComplete="off" noValidate={true}>
|
||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
</form>
|
||||
|
||||
{currentUploadedFile}
|
||||
{ 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>
|
||||
|
|
|
@ -15,17 +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.
|
||||
|
@ -45,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
|
||||
|
@ -76,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,10 +102,10 @@ export class BannedUser extends React.Component<IBannedUserProps> {
|
|||
const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
|
||||
return (
|
||||
<li>
|
||||
{unbanButton}
|
||||
<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 : ""}
|
||||
{ unbanButton }
|
||||
<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>
|
||||
</li>
|
||||
);
|
||||
|
@ -176,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(
|
||||
|
@ -203,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(
|
||||
|
@ -215,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', '');
|
||||
|
@ -277,13 +273,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
|
||||
);
|
||||
|
||||
let privilegedUsersSection = <div>{_t('No users have specific privileges in this room')}</div>;
|
||||
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
const privilegedUsers = [];
|
||||
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(
|
||||
|
@ -323,14 +320,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
privilegedUsersSection =
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<div className='mx_SettingsTab_subheading'>{ _t('Privileged Users') }</div>
|
||||
{privilegedUsers}
|
||||
{ privilegedUsers }
|
||||
</div>;
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection =
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<div className='mx_SettingsTab_subheading'>{ _t('Muted Users') }</div>
|
||||
{mutedUsers}
|
||||
{ mutedUsers }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -343,7 +340,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<div className='mx_SettingsTab_subheading'>{ _t('Banned users') }</div>
|
||||
<ul>
|
||||
{banned.map((member) => {
|
||||
{ banned.map((member) => {
|
||||
const banEvent = member.events.member.getContent();
|
||||
const sender = room.getMember(member.events.member.getSender());
|
||||
let bannedBy = member.events.member.getSender(); // start by falling back to mxid
|
||||
|
@ -354,7 +351,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
by={bannedBy}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
}) }
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
@ -394,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}>
|
||||
|
@ -412,15 +409,15 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
|
||||
{privilegedUsersSection}
|
||||
{mutedUsersSection}
|
||||
{bannedUsersSection}
|
||||
<div className="mx_SettingsTab_heading">{ _t("Roles & Permissions") }</div>
|
||||
{ privilegedUsersSection }
|
||||
{ mutedUsersSection }
|
||||
{ bannedUsersSection }
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Permissions")}</span>
|
||||
<p>{_t('Select the roles required to change various parts of the room')}</p>
|
||||
{powerSelectors}
|
||||
{eventPowerSelectors}
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Permissions") }</span>
|
||||
<p>{ _t('Select the roles required to change various parts of the room') }</p>
|
||||
{ powerSelectors }
|
||||
{ eventPowerSelectors }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
@ -75,7 +78,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
async UNSAFE_componentWillMount() { // eslint-disable-line
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
|
@ -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(
|
||||
|
@ -132,23 +135,23 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
{
|
||||
a: sub => <a href="https://element.io/help#encryption"
|
||||
rel="noreferrer noopener" target="_blank"
|
||||
>{sub}</a>,
|
||||
>{ sub }</a>,
|
||||
},
|
||||
),
|
||||
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 });
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -266,8 +289,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("Guests cannot join this room even if explicitly invited.")}
|
||||
<a href="" onClick={this.fixGuestAccess}>{_t("Click here to fix")}</a>
|
||||
{ _t("Guests cannot join this room even if explicitly invited.") }
|
||||
<a href="" onClick={this.fixGuestAccess}>{ _t("Click here to fix") }</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -279,7 +302,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("To link to this room, please add an address.")}
|
||||
{ _t("To link to this room, please add an address.") }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -287,8 +310,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
return (
|
||||
<div>
|
||||
{guestWarning}
|
||||
{aliasWarning}
|
||||
{ guestWarning }
|
||||
{ aliasWarning }
|
||||
<StyledRadioGroup
|
||||
name="roomVis"
|
||||
value={joinRule}
|
||||
|
@ -324,46 +347,47 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
const state = client.getRoom(this.props.roomId).currentState;
|
||||
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: HistoryVisibility.Shared,
|
||||
label: _t('Members only (since the point in time of selecting this option)'),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Invited,
|
||||
label: _t('Members only (since they were invited)'),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Joined,
|
||||
label: _t('Members only (since they joined)'),
|
||||
},
|
||||
];
|
||||
|
||||
// World readable doesn't make sense for encrypted rooms
|
||||
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
|
||||
options.unshift({
|
||||
value: HistoryVisibility.WorldReadable,
|
||||
label: _t("Anyone"),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{_t('Changes to who can read history will only apply to future messages in this room. ' +
|
||||
'The visibility of existing history will be unchanged.')}
|
||||
{ _t('Changes to who can read history will only apply to future messages in this room. ' +
|
||||
'The visibility of existing history will be unchanged.') }
|
||||
</div>
|
||||
<StyledRadioGroup
|
||||
name="historyVis"
|
||||
value={history}
|
||||
onChange={this.onHistoryRadioToggle}
|
||||
definitions={[
|
||||
{
|
||||
value: HistoryVisibility.WorldReadable,
|
||||
disabled: !canChangeHistory,
|
||||
label: _t("Anyone"),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Shared,
|
||||
disabled: !canChangeHistory,
|
||||
label: _t('Members only (since the point in time of selecting this option)'),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Invited,
|
||||
disabled: !canChangeHistory,
|
||||
label: _t('Members only (since they were invited)'),
|
||||
},
|
||||
{
|
||||
value: HistoryVisibility.Joined,
|
||||
disabled: !canChangeHistory,
|
||||
label: _t('Members only (since they joined)'),
|
||||
},
|
||||
]}
|
||||
disabled={!canChangeHistory}
|
||||
definitions={options}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const isEncrypted = this.state.encrypted;
|
||||
|
@ -381,9 +405,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
let historySection = (<>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Who can read history?") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{this.renderHistory()}
|
||||
{ this.renderHistory() }
|
||||
</div>
|
||||
</>);
|
||||
if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
|
||||
|
@ -392,27 +416,27 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Encryption")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Encryption") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SecurityRoomSettingsTab_encryptionSection'>
|
||||
<div>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<span>{_t("Once enabled, encryption cannot be disabled.")}</span>
|
||||
<span>{ _t("Once enabled, encryption cannot be disabled.") }</span>
|
||||
</div>
|
||||
<LabelledToggleSwitch value={isEncrypted} onChange={this.onEncryptionChange}
|
||||
label={_t("Encrypted")} disabled={!canEnableEncryption}
|
||||
/>
|
||||
</div>
|
||||
{encryptionSettings}
|
||||
{ encryptionSettings }
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Who can access this room?") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{this.renderRoomAccess()}
|
||||
{ this.renderRoomAccess() }
|
||||
</div>
|
||||
|
||||
{historySection}
|
||||
{ historySection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
@ -37,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
|||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import classNames from 'classnames';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
|
||||
|
@ -75,7 +77,8 @@ interface IState extends IThemeState {
|
|||
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
|
||||
|
||||
private themeTimer: NodeJS.Timeout;
|
||||
private themeTimer: number;
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -84,7 +87,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"),
|
||||
|
@ -101,6 +104,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
if (this.unmounted) return;
|
||||
|
||||
this.setState({
|
||||
userId,
|
||||
|
@ -109,6 +113,10 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private calculateThemeState(): IThemeState {
|
||||
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
|
||||
// show the right values for things.
|
||||
|
@ -149,43 +157,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 }),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -196,7 +204,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> => {
|
||||
|
@ -213,37 +221,50 @@ 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 onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let layout;
|
||||
switch (e.target.value) {
|
||||
case "irc": layout = Layout.IRC; break;
|
||||
case "group": layout = Layout.Group; break;
|
||||
case "bubble": layout = Layout.Bubble; break;
|
||||
}
|
||||
|
||||
this.setState({ layout: layout });
|
||||
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||
};
|
||||
|
||||
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();
|
||||
|
@ -254,7 +275,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
checked={this.state.useSystemTheme}
|
||||
onChange={(e) => this.onUseSystemThemeChanged(e.target.checked)}
|
||||
>
|
||||
{SettingsStore.getDisplayName("use_system_theme")}
|
||||
{ SettingsStore.getDisplayName("use_system_theme") }
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
}
|
||||
|
@ -264,9 +285,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
let messageElement = null;
|
||||
if (this.state.customThemeMessage.text) {
|
||||
if (this.state.customThemeMessage.isError) {
|
||||
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
|
||||
messageElement = <div className='text-error'>{ this.state.customThemeMessage.text }</div>;
|
||||
} else {
|
||||
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
|
||||
messageElement = <div className='text-success'>{ this.state.customThemeMessage.text }</div>;
|
||||
}
|
||||
}
|
||||
customThemeForm = (
|
||||
|
@ -284,8 +305,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
onClick={this.onAddCustomTheme}
|
||||
type="submit" kind="primary_sm"
|
||||
disabled={!this.state.customThemeUrl.trim()}
|
||||
>{_t("Add theme")}</AccessibleButton>
|
||||
{messageElement}
|
||||
>{ _t("Add theme") }</AccessibleButton>
|
||||
{ messageElement }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
@ -293,15 +314,15 @@ 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) => compare(a.name, b.name));
|
||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||
{systemThemeSection}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Theme") }</span>
|
||||
{ systemThemeSection }
|
||||
<div className="mx_ThemeSelectors">
|
||||
<StyledRadioGroup
|
||||
name="theme"
|
||||
|
@ -316,7 +337,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
outlined
|
||||
/>
|
||||
</div>
|
||||
{customThemeForm}
|
||||
{ customThemeForm }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -324,7 +345,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
private renderFontSection() {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Font size") }</span>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
|
@ -348,7 +369,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}
|
||||
/>
|
||||
|
||||
|
@ -360,22 +381,93 @@ 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"
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.IRC}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="irc"
|
||||
checked={this.state.layout === Layout.IRC}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("IRC") }
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Group}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="group"
|
||||
checked={this.state.layout == Layout.Group}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Modern") }
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
layout={Layout.Bubble}
|
||||
userId={this.state.userId}
|
||||
displayName={this.state.displayName}
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="bubble"
|
||||
checked={this.state.layout == Layout.Bubble}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{ _t("Message bubbles") }
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
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")}
|
||||
{ this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
@ -390,20 +482,23 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
name="useCompactLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
disabled={this.state.layout == Layout.IRC}
|
||||
disabled={this.state.layout !== Layout.Group}
|
||||
/>
|
||||
<StyledCheckbox
|
||||
checked={this.state.layout == Layout.IRC}
|
||||
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
||||
>
|
||||
{_t("Enable experimental, compact IRC style layout")}
|
||||
</StyledCheckbox>
|
||||
|
||||
{ !SettingsStore.getValue("feature_new_layout_switcher") ?
|
||||
<StyledCheckbox
|
||||
checked={this.state.layout == Layout.IRC}
|
||||
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
||||
>
|
||||
{ _t("Enable experimental, compact IRC style layout") }
|
||||
</StyledCheckbox> : null
|
||||
}
|
||||
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useSystemFont: checked})}
|
||||
onChange={(checked) => this.setState({ useSystemFont: checked })}
|
||||
/>
|
||||
<Field
|
||||
className="mx_AppearanceUserSettingsTab_systemFont"
|
||||
|
@ -423,8 +518,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</>;
|
||||
}
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
|
||||
{toggle}
|
||||
{advanced}
|
||||
{ toggle }
|
||||
{ advanced }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -433,13 +528,14 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Customise your appearance")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
|
||||
<div className="mx_SettingsTab_SubHeading">
|
||||
{_t("Appearance Settings only affect this %(brand)s session.", { brand })}
|
||||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||
</div>
|
||||
{this.renderThemeSection()}
|
||||
{this.renderFontSection()}
|
||||
{this.renderAdvancedSection()}
|
||||
{ this.renderThemeSection() }
|
||||
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
||||
{ this.renderFontSection() }
|
||||
{ this.renderAdvancedSection() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@ 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 {
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<span className="mx_SettingsTab_heading">{_t("Flair")}</span>
|
||||
<span className="mx_SettingsTab_heading">{ _t("Flair") }</span>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<GroupUserSettings />
|
||||
</div>
|
||||
|
|
|
@ -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) {
|
||||
|
@ -289,11 +289,11 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
onMsisdnsChange={this._onMsisdnsChange}
|
||||
/>;
|
||||
threepidSection = <div>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
|
||||
{emails}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||
{ emails }
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
|
||||
{msisdns}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
|
||||
{ msisdns }
|
||||
</div>;
|
||||
} else if (this.state.serverSupportsSeparateAddAndBind === null) {
|
||||
threepidSection = <Spinner />;
|
||||
|
@ -308,12 +308,12 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Account") }</span>
|
||||
<p className="mx_SettingsTab_subsectionText">
|
||||
{passwordChangeText}
|
||||
{ passwordChangeText }
|
||||
</p>
|
||||
{passwordChangeForm}
|
||||
{threepidSection}
|
||||
{ passwordChangeForm }
|
||||
{ threepidSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
// TODO: Convert to new-styled Field
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
|
||||
<LanguageDropdown
|
||||
className="mx_GeneralUserSettingsTab_languageInput"
|
||||
onOptionChange={this._onLanguageChange}
|
||||
|
@ -335,7 +335,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
_renderSpellCheckSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
|
||||
<SpellCheckSettings
|
||||
languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange}
|
||||
|
@ -350,11 +350,11 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
if (this.state.requiredPolicyInfo.hasTerms) {
|
||||
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
|
||||
const intro = <span className="mx_SettingsTab_subsectionText">
|
||||
{_t(
|
||||
{ _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 (
|
||||
<div>
|
||||
|
@ -364,7 +364,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
onFinished={this.state.requiredPolicyInfo.resolve}
|
||||
introElement={intro}
|
||||
/>
|
||||
{ /* has its own heading as it includes the current ID server */ }
|
||||
{ /* has its own heading as it includes the current identity server */ }
|
||||
<SetIdServer missingTerms={true} />
|
||||
</div>
|
||||
);
|
||||
|
@ -377,17 +377,17 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
|
||||
|
||||
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
|
||||
{emails}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
|
||||
{ emails }
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
|
||||
{msisdns}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
|
||||
{ msisdns }
|
||||
</div> : null;
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
{threepidSection}
|
||||
{ /* has its own heading as it includes the current ID server */ }
|
||||
{ threepidSection }
|
||||
{ /* has its own heading as it includes the current identity server */ }
|
||||
<SetIdServer />
|
||||
</div>
|
||||
);
|
||||
|
@ -397,12 +397,12 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
// TODO: Improve warning text for account deactivation
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Account management") }</span>
|
||||
<span className="mx_SettingsTab_subsectionText">
|
||||
{_t("Deactivating your account is a permanent action - be careful!")}
|
||||
{ _t("Deactivating your account is a permanent action - be careful!") }
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
|
||||
{_t("Deactivate Account")}
|
||||
{ _t("Deactivate Account") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -434,28 +434,28 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
let accountManagementSection;
|
||||
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
||||
accountManagementSection = <>
|
||||
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
||||
{this._renderManagementSection()}
|
||||
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
|
||||
{ this._renderManagementSection() }
|
||||
</>;
|
||||
}
|
||||
|
||||
let discoverySection;
|
||||
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
discoverySection = <>
|
||||
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
|
||||
{this._renderDiscoverySection()}
|
||||
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
|
||||
{ this._renderDiscoverySection() }
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("General")}</div>
|
||||
{this._renderProfileSection()}
|
||||
{this._renderAccountSection()}
|
||||
{this._renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
|
||||
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||
{ this._renderProfileSection() }
|
||||
{ this._renderAccountSection() }
|
||||
{ this._renderLanguageSection() }
|
||||
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
|
||||
{ discoverySection }
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
{ this._renderIntegrationManagerSection() /* Has its own title */ }
|
||||
{ accountManagementSection }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,14 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
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";
|
||||
|
@ -30,6 +29,8 @@ 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: () => void;
|
||||
|
@ -54,10 +55,10 @@ 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);
|
||||
});
|
||||
}
|
||||
|
@ -93,10 +94,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, {});
|
||||
};
|
||||
|
||||
|
@ -127,15 +124,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
const legalLinks = [];
|
||||
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
|
||||
legalLinks.push(<div key={tocEntry.url}>
|
||||
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
|
||||
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Legal") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{legalLinks}
|
||||
{ legalLinks }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -146,7 +143,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
// Also, is ugly but necessary.
|
||||
return (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Credits") }</span>
|
||||
<ul>
|
||||
<li>
|
||||
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener"
|
||||
|
@ -183,8 +180,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
|
||||
const successful = await copyPlaintext(text);
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
|
@ -214,14 +210,14 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{sub}
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
|
||||
faqText = (
|
||||
<div>
|
||||
{_t(
|
||||
{ _t(
|
||||
'For help with using %(brand)s, click <a>here</a> or start a chat with our ' +
|
||||
'bot using the button below.',
|
||||
{
|
||||
|
@ -233,13 +229,13 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
rel='noreferrer noopener'
|
||||
target='_blank'
|
||||
>
|
||||
{sub}
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
<div>
|
||||
<AccessibleButton onClick={this.onStartBotChat} kind='primary'>
|
||||
{_t("Chat with %(brand)s Bot", { brand })}
|
||||
{ _t("Chat with %(brand)s Bot", { brand }) }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -255,29 +251,29 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportingSection = (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t('Bug reporting') }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t(
|
||||
{ _t(
|
||||
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||
"us track down the problem. Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited and the usernames of " +
|
||||
"other users. They do not contain messages.",
|
||||
)}
|
||||
) }
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this.onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
{ _t("Submit debug logs") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{_t(
|
||||
{ _t(
|
||||
"To report a Matrix-related security issue, please read the Matrix.org " +
|
||||
"<a>Security Disclosure Policy</a>.", {},
|
||||
{
|
||||
a: sub => <a href="https://matrix.org/security-disclosure-policy/"
|
||||
rel="noreferrer noopener" target="_blank"
|
||||
>{sub}</a>,
|
||||
>{ sub }</a>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -287,19 +283,19 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
|
||||
{ bugReportingSection }
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("FAQ") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{faqText}
|
||||
{ faqText }
|
||||
</div>
|
||||
<AccessibleButton kind="primary" onClick={KeyboardShortcuts.toggleDialog}>
|
||||
{ _t("Keyboard Shortcuts") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<div className="mx_HelpUserSettingsTab_copy">
|
||||
{ appVersion }<br />
|
||||
|
@ -310,23 +306,23 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
className="mx_HelpUserSettingsTab_copyButton"
|
||||
/>
|
||||
</div>
|
||||
{updateButton}
|
||||
{ updateButton }
|
||||
</div>
|
||||
</div>
|
||||
{this.renderLegal()}
|
||||
{this.renderCredits()}
|
||||
{ this.renderLegal() }
|
||||
{ this.renderCredits() }
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Advanced") }</span>
|
||||
<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("Homeserver is") } <code>{ MatrixClientPeg.get().getHomeserverUrl() }</code><br />
|
||||
{ _t("Identity server is") } <code>{ MatrixClientPeg.get().getIdentityServerUrl() }</code><br />
|
||||
<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>
|
||||
<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_copy">
|
||||
<code>{MatrixClientPeg.get().getAccessToken()}</code>
|
||||
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onAccessTokenCopyClick}
|
||||
|
@ -336,7 +332,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
</details><br />
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
{ _t("Clear cache and reload") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,13 +15,13 @@ 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";
|
||||
|
||||
|
@ -69,7 +69,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
|
||||
|
||||
labsSection = <div className="mx_SettingsTab_section">
|
||||
{flags}
|
||||
{ flags }
|
||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||
|
@ -79,7 +79,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Labs") }</div>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{
|
||||
_t('Feeling experimental? Labs are the best way to get things early, ' +
|
||||
|
@ -87,7 +87,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
'<a>Learn more</a>.', {}, {
|
||||
'a': (sub) => {
|
||||
return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
||||
rel='noreferrer noopener' target='_blank'>{sub}</a>;
|
||||
rel='noreferrer noopener' target='_blank'>{ sub }</a>;
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,84 +84,79 @@ 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;
|
||||
|
||||
const renderRules = (rules: ListRule[]) => {
|
||||
if (rules.length === 0) return <i>{_t("None")}</i>;
|
||||
if (rules.length === 0) return <i>{ _t("None") }</i>;
|
||||
|
||||
const tiles = [];
|
||||
for (const rule of rules) {
|
||||
tiles.push(<li key={rule.kind + rule.entity}><code>{rule.entity}</code></li>);
|
||||
tiles.push(<li key={rule.kind + rule.entity}><code>{ rule.entity }</code></li>);
|
||||
}
|
||||
return <ul>{tiles}</ul>;
|
||||
return <ul>{ tiles }</ul>;
|
||||
};
|
||||
|
||||
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>
|
||||
{renderRules(list.serverRules)}
|
||||
<h3>{_t("User rules")}</h3>
|
||||
{renderRules(list.userRules)}
|
||||
<h3>{ _t("Server rules") }</h3>
|
||||
{ renderRules(list.serverRules) }
|
||||
<h3>{ _t("User rules") }</h3>
|
||||
{ renderRules(list.userRules) }
|
||||
</div>
|
||||
),
|
||||
button: _t("Close"),
|
||||
|
@ -168,11 +165,9 @@ 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>;
|
||||
if (!list || rules.length <= 0) return <i>{ _t("You have not ignored anyone.") }</i>;
|
||||
|
||||
const tiles = [];
|
||||
for (const rule of rules) {
|
||||
|
@ -183,34 +178,32 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
onClick={() => this.removePersonalRule(rule)}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<code>{rule.entity}</code>
|
||||
<code>{ rule.entity }</code>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("You are currently ignoring:")}</p>
|
||||
<ul>{tiles}</ul>
|
||||
<p>{ _t("You are currently ignoring:") }</p>
|
||||
<ul>{ tiles }</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
|
||||
if (!lists || lists.length <= 0) return <i>{ _t("You are not subscribed to any lists") }</i>;
|
||||
|
||||
const tiles = [];
|
||||
for (const list of lists) {
|
||||
const room = MatrixClientPeg.get().getRoom(list.roomId);
|
||||
const name = room ? <span>{room.name} (<code>{list.roomId}</code>)</span> : <code>list.roomId</code>;
|
||||
const name = room ? <span>{ room.name } (<code>{ list.roomId }</code>)</span> : <code>list.roomId</code>;
|
||||
tiles.push(
|
||||
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
|
||||
<AccessibleButton
|
||||
|
@ -218,66 +211,64 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
onClick={() => this.unsubscribeFromList(list)}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("Unsubscribe")}
|
||||
{ _t("Unsubscribe") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary_sm"
|
||||
onClick={() => this.viewListRules(list)}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("View rules")}
|
||||
{ _t("View rules") }
|
||||
</AccessibleButton>
|
||||
{name}
|
||||
{ name }
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("You are currently subscribed to:")}</p>
|
||||
<ul>{tiles}</ul>
|
||||
<p>{ _t("You are currently subscribed to:") }</p>
|
||||
<ul>{ tiles }</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Ignored users") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
|
||||
<span className='warning'>{ _t("⚠ These settings are meant for advanced users.") }</span><br />
|
||||
<br />
|
||||
{_t(
|
||||
{ _t(
|
||||
"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>},
|
||||
)}<br />
|
||||
{ brand }, { code: (s) => <code>{ s }</code> },
|
||||
) }<br />
|
||||
<br />
|
||||
{_t(
|
||||
{ _t(
|
||||
"Ignoring people is done through ban lists which contain rules for " +
|
||||
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
|
||||
"that list will be hidden from you.",
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Personal ban list") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Your personal ban list holds all the users/servers you personally don't " +
|
||||
"want to see messages from. After ignoring your first user/server, a new room " +
|
||||
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
|
||||
"the ban list in effect.",
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
<div>
|
||||
{this.renderPersonalBanListRules()}
|
||||
{ this.renderPersonalBanListRules() }
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={this.onAddPersonalRule} autoComplete="off">
|
||||
|
@ -294,22 +285,22 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
onClick={this.onAddPersonalRule}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("Ignore")}
|
||||
{ _t("Ignore") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Subscribed lists") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<span className='warning'>{_t("Subscribing to a ban list will cause you to join it!")}</span>
|
||||
<span className='warning'>{ _t("Subscribing to a ban list will cause you to join it!") }</span>
|
||||
|
||||
<span>{_t(
|
||||
<span>{ _t(
|
||||
"If this isn't what you want, please use a different tool to ignore users.",
|
||||
)}</span>
|
||||
) }</span>
|
||||
</div>
|
||||
<div>
|
||||
{this.renderSubscribedBanLists()}
|
||||
{ this.renderSubscribedBanLists() }
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={this.onSubscribeList} autoComplete="off">
|
||||
|
@ -325,7 +316,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
onClick={this.onSubscribeList}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("Subscribe")}
|
||||
{ _t("Subscribe") }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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,21 +15,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import * as sdk from "../../../../../index";
|
||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import Notifications from "../../Notifications";
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
||||
export default class NotificationUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
render() {
|
||||
const Notifications = sdk.getComponent("views.settings.Notifications");
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<Notifications />
|
||||
</div>
|
|
@ -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} />;
|
||||
});
|
||||
|
@ -215,30 +224,53 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
|
||||
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
<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("Timeline")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
<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("General")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
|
||||
{minimizeToTrayOption}
|
||||
{autoHideMenuOption}
|
||||
{autoLaunchOption}
|
||||
{warnBeforeExitOption}
|
||||
<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) }
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("General") }</span>
|
||||
{ this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS) }
|
||||
{ minimizeToTrayOption }
|
||||
{ autoHideMenuOption }
|
||||
{ autoLaunchOption }
|
||||
{ warnBeforeExitOption }
|
||||
<Field
|
||||
label={_t('Autocomplete delay (ms)')}
|
||||
type='number'
|
||||
|
|
|
@ -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") {
|
||||
|
@ -215,10 +215,10 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
importExportButtons = (
|
||||
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
||||
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
|
||||
{_t("Export E2E room keys")}
|
||||
{ _t("Export E2E room keys") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
|
||||
{_t("Import E2E room keys")}
|
||||
{ _t("Import E2E room keys") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -235,25 +235,25 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Cryptography") }</span>
|
||||
<ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
|
||||
<li>
|
||||
<label>{_t("Session ID:")}</label>
|
||||
<span><code>{deviceId}</code></span>
|
||||
<label>{ _t("Session ID:") }</label>
|
||||
<span><code>{ deviceId }</code></span>
|
||||
</li>
|
||||
<li>
|
||||
<label>{_t("Session key:")}</label>
|
||||
<span><code><b>{identityKey}</b></code></span>
|
||||
<label>{ _t("Session key:") }</label>
|
||||
<span><code><b>{ identityKey }</b></code></span>
|
||||
</li>
|
||||
</ul>
|
||||
{importExportButtons}
|
||||
{noSendUnverifiedSetting}
|
||||
{ importExportButtons }
|
||||
{ noSendUnverifiedSetting }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderIgnoredUsers() {
|
||||
const {waitingUnignored, ignoredUserIds} = this.state;
|
||||
const { waitingUnignored, ignoredUserIds } = this.state;
|
||||
|
||||
const userIds = !ignoredUserIds?.length
|
||||
? _t('You have no ignored users.')
|
||||
|
@ -270,9 +270,9 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t('Ignored users') }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{userIds}
|
||||
{ userIds }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -289,14 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
|
||||
return (
|
||||
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
|
||||
<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 />}
|
||||
{ this.state.managingInvites ? <InlineSpinner /> : <div /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -309,7 +309,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
const secureBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Secure Backup") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<SecureBackupPanel />
|
||||
</div>
|
||||
|
@ -318,7 +318,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
const eventIndex = (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Message search") }</span>
|
||||
<EventIndexPanel />
|
||||
</div>
|
||||
);
|
||||
|
@ -330,7 +330,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
||||
const crossSigning = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<CrossSigningPanel />
|
||||
</div>
|
||||
|
@ -348,19 +348,19 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
let privacySection;
|
||||
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
|
||||
privacySection = <React.Fragment>
|
||||
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Privacy") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Analytics") }</span>
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
{_t(
|
||||
{ _t(
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.",
|
||||
{ brand },
|
||||
)}
|
||||
) }
|
||||
|
||||
{_t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.")}
|
||||
{ _t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.") }
|
||||
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
|
||||
{_t("Learn more about how we use analytics.")}
|
||||
{ _t("Learn more about how we use analytics.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
||||
|
@ -377,11 +377,11 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
// only show the section if there's something to show
|
||||
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
||||
advancedSection = <>
|
||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ignoreUsersPanel}
|
||||
{invitesPanel}
|
||||
{e2ePanel}
|
||||
{ ignoreUsersPanel }
|
||||
{ invitesPanel }
|
||||
{ e2ePanel }
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
@ -389,31 +389,31 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{warning}
|
||||
<div className="mx_SettingsTab_heading">{_t("Where you’re logged in")}</div>
|
||||
{ warning }
|
||||
<div className="mx_SettingsTab_heading">{ _t("Where you’re logged in") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Manage the names of and sign out of your sessions below or " +
|
||||
"<a>verify them in your User Profile</a>.", {},
|
||||
{
|
||||
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
|
||||
{sub}
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t("A session's public name is visible to people you communicate with")}
|
||||
{ _t("A session's public name is visible to people you communicate with") }
|
||||
<DevicesPanel />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{secureBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
{ secureBackup }
|
||||
{ eventIndex }
|
||||
{ crossSigning }
|
||||
{ this._renderCurrentDeviceInfo() }
|
||||
</div>
|
||||
{ privacySection }
|
||||
{ advancedSection }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
206
src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
Normal file
206
src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue