Merge remote-tracking branch 'upstream/develop' into feature-surround-with

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-20 13:21:52 +02:00
commit 621aee6f9a
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
883 changed files with 21817 additions and 16608 deletions

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,68 +15,76 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
interface IProps {
roomId: string;
closeSettingsFn(): void;
}
interface IRecommendedVersion {
version: string;
needsUpgrade: boolean;
urgent: boolean;
}
interface IState {
upgradeRecommendation?: IRecommendedVersion;
oldRoomId?: string;
oldEventId?: string;
upgraded?: boolean;
}
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
export default class AdvancedRoomSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
closeSettingsFn: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
export default class AdvancedRoomSettingsTab extends React.Component<IProps, IState> {
constructor(props, context) {
super(props, context);
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
};
}
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
// we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room.getRecommendedVersion().then((v) => {
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
const additionalStateChanges = {};
const createEvent = room.currentState.getStateEvents("m.room.create", "");
const additionalStateChanges: Partial<IState> = {};
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
if (predecessor && predecessor.room_id) {
additionalStateChanges['oldRoomId'] = predecessor.room_id;
additionalStateChanges['oldEventId'] = predecessor.event_id;
additionalStateChanges['hasPreviousRoom'] = true;
additionalStateChanges.oldRoomId = predecessor.room_id;
additionalStateChanges.oldEventId = predecessor.event_id;
}
this.setState({
upgraded: tombstone && tombstone.getContent().replacement_room,
upgraded: !!tombstone?.getContent().replacement_room,
upgradeRecommendation: v,
...additionalStateChanges,
});
});
}
_upgradeRoom = (e) => {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
private upgradeRoom = (e) => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
};
_openDevtools = (e) => {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
private openDevtools = (e) => {
Modal.createDialog(DevtoolsDialog, { roomId: this.props.roomId });
};
_onOldRoomClicked = (e) => {
private onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
const room = client.getRoom(this.props.roomId);
let unfederatableSection;
const createEvent = room.currentState.getStateEvents('m.room.create', '');
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
unfederatableSection = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
}
let roomUpgradeButton;
@ -103,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>&nbsp;
{this.props.roomId}
<span>{ _t("Internal room ID:") }</span>&nbsp;
{ this.props.roomId }
</div>
{unfederatableSection}
{ unfederatableSection }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
<div>
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
<span>{ _t("Room version:") }</span>&nbsp;
{ room.getVersion() }
</div>
{oldRoomLink}
{roomUpgradeButton}
{ oldRoomLink }
{ roomUpgradeButton }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
<AccessibleButton onClick={this._openDevtools} kind='primary'>
{_t("Open Devtools")}
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
<AccessibleButton onClick={this.openDevtools} kind='primary'>
{ _t("Open Devtools") }
</AccessibleButton>
</div>
</div>

View file

@ -15,13 +15,13 @@ limitations under the License.
*/
import React from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import BridgeTile from "../../BridgeTile";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
const BRIDGE_EVENT_TYPES = [
"uk.half-shot.bridge",
@ -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 isnt 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>
);

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
export default class GeneralRoomSettingsTab extends React.Component {
@ -60,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') }

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
export default class NotificationsSettingsTab extends React.Component {
@ -47,7 +47,7 @@ export default class NotificationsSettingsTab extends React.Component {
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
this.setState({ currentSound: soundData.name || soundData.url });
}
async _triggerUploader(e) {
@ -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>

View file

@ -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>
);

View file

@ -16,32 +16,35 @@ limitations under the License.
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
// Knock and private are reserved keywords which are not yet implemented.
enum JoinRule {
export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
/**
* @deprecated Reserved. Should not be used.
*/
Private = "private",
}
enum GuestAccess {
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
enum HistoryVisibility {
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@ -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.")}&nbsp;
<a href="" onClick={this.fixGuestAccess}>{_t("Click here to fix")}</a>
{ _t("Guests cannot join this room even if explicitly invited.") }&nbsp;
<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>
);
}

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
import SettingsStore from "../../../../../settings/SettingsStore";
@ -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>
);
}

View file

@ -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>

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
import * as languageHandler from "../../../../../languageHandler";
import SettingsStore from "../../../../../settings/SettingsStore";
@ -27,19 +27,19 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types";
import PlatformPeg from "../../../../../PlatformPeg";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import { Service, startTermsFlow } from "../../../../../Terms";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
export default class GeneralUserSettingsTab extends React.Component {
@ -84,7 +84,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// the enabled flag value.
const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false;
this.setState({serverSupportsSeparateAddAndBind, canChangePassword});
this.setState({ serverSupportsSeparateAddAndBind, canChangePassword });
this._getThreepidState();
}
@ -104,7 +104,7 @@ export default class GeneralUserSettingsTab extends React.Component {
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) });
this._getThreepidState();
}
};
@ -145,7 +145,7 @@ export default class GeneralUserSettingsTab extends React.Component {
async _checkTerms() {
if (!this.state.haveIdServer) {
this.setState({idServerHasUnsignedTerms: false});
this.setState({ idServerHasUnsignedTerms: false });
return;
}
@ -191,7 +191,7 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.language === newLanguage) return;
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({language: newLanguage});
this.setState({ language: newLanguage });
const platform = PlatformPeg.get();
if (platform) {
platform.setLanguage(newLanguage);
@ -200,7 +200,7 @@ export default class GeneralUserSettingsTab extends React.Component {
};
_onSpellCheckLanguagesChange = (languages) => {
this.setState({spellCheckLanguages: languages});
this.setState({ spellCheckLanguages: languages });
const plaf = PlatformPeg.get();
if (plaf) {
@ -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>
);

View file

@ -15,14 +15,13 @@ limitations under the License.
*/
import React from 'react';
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../../createRoom";
import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import UpdateCheckButton from "../../UpdateCheckButton";
@ -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);
});
}
@ -81,10 +82,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
};
private onBugReport = (e) => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
@ -115,15 +112,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>
);
@ -134,7 +131,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
// Also, &nbsp; 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"
@ -171,13 +168,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
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'),
});
this.closeCopiedTooltip = target.onmouseleave = close;
}
};
render() {
const brand = SdkConfig.get().brand;
@ -193,14 +189,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.',
{
@ -212,13 +208,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>
@ -239,29 +235,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>
);
@ -269,39 +265,39 @@ 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'>
{_t("%(brand)s version:", { brand })} {appVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{updateButton}
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
{ _t("olm version:") } { olmVersion }<br />
{ 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_accessToken">
<code>{MatrixClientPeg.get().getAccessToken()}</code>
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onAccessTokenCopyClick}
@ -311,7 +307,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>

View file

@ -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>;
},
})
}

View file

@ -15,15 +15,18 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {Mjolnir} from "../../../../../mjolnir/Mjolnir";
import {ListRule} from "../../../../../mjolnir/ListRule";
import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
import { Mjolnir } from "../../../../../mjolnir/Mjolnir";
import { ListRule } from "../../../../../mjolnir/ListRule";
import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList";
import Modal from "../../../../../Modal";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import AccessibleButton from "../../../elements/AccessibleButton";
import Field from "../../../elements/Field";
interface IState {
busy: boolean;
@ -44,11 +47,11 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
}
private onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value});
this.setState({ newPersonalRule: e.target.value });
};
private onNewListChanged = (e) => {
this.setState({newList: e.target.value});
this.setState({ newList: e.target.value });
};
private onAddPersonalRule = async (e) => {
@ -60,21 +63,20 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
kind = RULE_USER;
}
this.setState({busy: true});
this.setState({ busy: true });
try {
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered
this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, {
title: _t('Error adding ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
this.setState({ busy: false });
}
};
@ -82,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>&nbsp;
<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>&nbsp;
<AccessibleButton
kind="primary_sm"
onClick={() => this.viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
{ _t("View rules") }
</AccessibleButton>&nbsp;
{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>
&nbsp;
<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>

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,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>

View file

@ -16,14 +16,16 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton";
interface IState {
autoLaunch: boolean;
@ -45,6 +47,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
'breadcrumbs',
];
static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
@ -54,28 +60,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',
@ -144,38 +154,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} />;
});
@ -216,30 +225,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'

View file

@ -17,24 +17,25 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import { sleep } from "matrix-js-sdk/src/utils";
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../../Analytics";
import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import {sleep} from "../../../../../utils/promise";
import dis from "../../../../../dispatcher/dispatcher";
import {privateShouldBeEncrypted} from "../../../../../createRoom";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { privateShouldBeEncrypted } from "../../../../../createRoom";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
import CountlyAnalytics from "../../../../../CountlyAnalytics";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -82,12 +83,11 @@ export default class SecurityUserSettingsTab extends React.Component {
this._onAction = this._onAction.bind(this);
}
_onAction({action}) {
_onAction({ action }) {
if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
this.setState({ignoredUserIds, waitingUnignored: newWaitingUnignored});
this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored });
}
}
@ -111,14 +111,14 @@ export default class SecurityUserSettingsTab extends React.Component {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
{ matrixClient: MatrixClientPeg.get() },
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
{ matrixClient: MatrixClientPeg.get() },
);
};
@ -131,13 +131,13 @@ export default class SecurityUserSettingsTab extends React.Component {
}
_onUserUnignored = async (userId) => {
const {ignoredUserIds, waitingUnignored} = this.state;
const { ignoredUserIds, waitingUnignored } = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
const index = currentlyIgnoredUserIds.indexOf(userId);
if (index !== -1) {
currentlyIgnoredUserIds.splice(index, 1);
this.setState(({waitingUnignored}) => ({waitingUnignored: [...waitingUnignored, userId]}));
this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] }));
MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds);
}
};
@ -168,7 +168,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// Accept/reject invite
await action(roomId).then(() => {
// No error, update invited rooms button
this.setState({invitedRoomAmt: self.state.invitedRoomAmt - 1});
this.setState({ invitedRoomAmt: self.state.invitedRoomAmt - 1 });
}, async (e) => {
// Action failure
if (e.errcode === "M_LIMIT_EXCEEDED") {
@ -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 },
)}
) }
&nbsp;
{_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 youre logged in")}</div>
{ warning }
<div className="mx_SettingsTab_heading">{ _t("Where youre 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 }

View file

@ -1,234 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import Modal from "../../../../../Modal";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
export default class VoiceUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
mediaDevices: false,
activeAudioOutput: null,
activeAudioInput: null,
activeVideoInput: null,
};
}
async componentDidMount() {
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this._refreshMediaDevices();
}
}
_refreshMediaDevices = async (stream) => {
this.setState({
mediaDevices: await CallMediaHandler.getDevices(),
activeAudioOutput: CallMediaHandler.getAudioOutput(),
activeAudioInput: CallMediaHandler.getAudioInput(),
activeVideoInput: CallMediaHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
// so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
};
_requestMediaPermissions = async () => {
let constraints;
let stream;
let error;
try {
constraints = {video: true, audio: true};
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
this._refreshMediaDevices(stream);
}
};
_setAudioOutput = (e) => {
CallMediaHandler.setAudioOutput(e.target.value);
this.setState({
activeAudioOutput: e.target.value,
});
};
_setAudioInput = (e) => {
CallMediaHandler.setAudioInput(e.target.value);
this.setState({
activeAudioInput: e.target.value,
});
};
_setVideoInput = (e) => {
CallMediaHandler.setVideoInput(e.target.value);
this.setState({
activeVideoInput: e.target.value,
});
};
_changeWebRtcMethod = (p2p) => {
MatrixClientPeg.get().setForceTURN(!p2p);
};
_changeFallbackICEServerAllowed = (allow) => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
};
_renderDeviceOptions(devices, category) {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
});
}
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
let requestButton = null;
let speakerDropdown = null;
let microphoneDropdown = null;
let webcamDropdown = null;
if (this.state.mediaDevices === false) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
const defaultOption = {
deviceId: '',
label: _t('Default Device'),
};
const getDefaultDevice = (devices) => {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift(defaultOption);
return '';
} else {
return 'default';
}
};
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = (
<Field element="select" label={_t("Audio Output")}
value={this.state.activeAudioOutput || defaultDevice}
onChange={this._setAudioOutput}>
{this._renderDeviceOptions(audioOutputs, 'audioOutput')}
</Field>
);
}
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = (
<Field element="select" label={_t("Microphone")}
value={this.state.activeAudioInput || defaultDevice}
onChange={this._setAudioInput}>
{this._renderDeviceOptions(audioInputs, 'audioInput')}
</Field>
);
}
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = (
<Field element="select" label={_t("Camera")}
value={this.state.activeVideoInput || defaultDevice}
onChange={this._setVideoInput}>
{this._renderDeviceOptions(videoInputs, 'videoInput')}
</Field>
);
}
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section">
{requestButton}
{speakerDropdown}
{microphoneDropdown}
{webcamDropdown}
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this._changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this._changeFallbackICEServerAllowed}
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,206 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
import ErrorDialog from '../../../dialogs/ErrorDialog';
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift({ deviceId: '', label: _t('Default Device') });
return '';
} else {
return 'default';
}
};
interface IState extends Record<MediaDeviceKindEnum, string> {
mediaDevices: IMediaDevices;
}
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
mediaDevices: null,
[MediaDeviceKindEnum.AudioOutput]: null,
[MediaDeviceKindEnum.AudioInput]: null,
[MediaDeviceKindEnum.VideoInput]: null,
};
}
async componentDidMount() {
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this.refreshMediaDevices();
}
}
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({
mediaDevices: await MediaDeviceHandler.getDevices(),
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
// so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
};
private requestMediaPermissions = async (): Promise<void> => {
let constraints;
let stream;
let error;
try {
constraints = { video: true, audio: true };
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t(
'You may need to manually permit %(brand)s to access your microphone/webcam',
{ brand },
),
});
} else {
this.refreshMediaDevices(stream);
}
};
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
MediaDeviceHandler.instance.setDevice(deviceId, kind);
this.setState<null>({ [kind]: deviceId });
};
private changeWebRtcMethod = (p2p: boolean): void => {
MatrixClientPeg.get().setForceTURN(!p2p);
};
private changeFallbackICEServerAllowed = (allow: boolean): void => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
};
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{ d.label }</option>);
});
}
private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
const devices = this.state.mediaDevices[kind].slice(0);
if (devices.length === 0) return null;
const defaultDevice = getDefaultDevice(devices);
return (
<Field
element="select"
label={label}
value={this.state[kind] || defaultDevice}
onChange={(e) => this.setDevice(e.target.value, kind)}
>
{ this.renderDeviceOptions(devices, kind) }
</Field>
);
}
render() {
let requestButton = null;
let speakerDropdown = null;
let microphoneDropdown = null;
let webcamDropdown = null;
if (!this.state.mediaDevices) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{ _t("Missing media permissions, click the button below to request.") }</p>
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{ _t("Request media permissions") }
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
<p>{ _t('No Audio Outputs detected') }</p>
);
microphoneDropdown = (
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
<p>{ _t('No Microphones detected') }</p>
);
webcamDropdown = (
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
<p>{ _t('No Webcams detected') }</p>
);
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
<div className="mx_SettingsTab_section">
{ requestButton }
{ speakerDropdown }
{ microphoneDropdown }
{ webcamDropdown }
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this.changeFallbackICEServerAllowed}
/>
</div>
</div>
);
}
}