Merge remote-tracking branch 'upstream/develop' into public-e2ee-warn

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-12 18:00:34 +02:00
commit 0618460833
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
874 changed files with 24858 additions and 16550 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,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
roomUpgradeButton = (
<div>
<p className='mx_SettingsTab_warningText'>
{_t(
{ _t(
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
"b": (sub) => <b>{sub}</b>,
"i": (sub) => <i>{sub}</i>,
},
)}
) }
</p>
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
{_t("Upgrade this room to the recommended room version")}
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
{ _t("Upgrade this room to the recommended room version") }
</AccessibleButton>
</div>
);
}
let oldRoomLink;
if (this.state.hasPreviousRoom) {
if (this.state.oldRoomId) {
let name = _t("this room");
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room && room.name) name = room.name;
oldRoomLink = (
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
{_t("View older messages in %(roomName)s.", {roomName: name})}
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
<span className='mx_SettingsTab_subheading'>
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
</span>
<div>
<span>{_t("Internal room ID:")}</span>&nbsp;
{this.props.roomId}
<span>{ _t("Internal room ID:") }</span>&nbsp;
{ this.props.roomId }
</div>
{unfederatableSection}
{ unfederatableSection }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
<div>
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
<span>{ _t("Room version:") }</span>&nbsp;
{ room.getVersion() }
</div>
{oldRoomLink}
{roomUpgradeButton}
{ oldRoomLink }
{ roomUpgradeButton }
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
<AccessibleButton onClick={this._openDevtools} kind='primary'>
{_t("Open Devtools")}
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
<AccessibleButton onClick={this.openDevtools} kind='primary'>
{ _t("Open Devtools") }
</AccessibleButton>
</div>
</div>

View file

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

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
export default class GeneralRoomSettingsTab extends React.Component {
@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
<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>
{ flairSection }

View file

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

View file

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

View file

@ -16,32 +16,35 @@ limitations under the License.
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
// Knock and private are reserved keywords which are not yet implemented.
enum JoinRule {
export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
/**
* @deprecated Reserved. Should not be used.
*/
Private = "private",
}
enum GuestAccess {
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
enum HistoryVisibility {
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@ -97,9 +100,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
HistoryVisibility.Shared,
);
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
this.setState({joinRule, guestAccess, history, encrypted});
this.setState({ joinRule, guestAccess, history, encrypted });
const hasAliases = await this.hasAliases();
this.setState({hasAliases});
this.setState({ hasAliases });
}
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
@ -123,7 +126,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onEncryptionChange = async (e: React.ChangeEvent) => {
if (this.state.joinRule == "public") {
const {finished} = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
const { finished } = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, {
title: _t('Enable encryption in a public room?'),
description: _t(
"Note that enabling encryption in public rooms renders the " +
@ -134,7 +137,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
});
const [confirm] = await finished;
if (!confirm) {
this.setState({encrypted: false});
this.setState({ encrypted: false });
return;
}
}
@ -154,18 +157,18 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
),
onFinished: (confirm) => {
if (!confirm) {
this.setState({encrypted: false});
this.setState({ encrypted: false });
return;
}
const beforeEncrypted = this.state.encrypted;
this.setState({encrypted: true});
this.setState({ encrypted: true });
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.encryption",
{ algorithm: "m.megolm.v1.aes-sha2" },
).catch((e) => {
console.error(e);
this.setState({encrypted: beforeEncrypted});
this.setState({ encrypted: beforeEncrypted });
});
},
});
@ -180,16 +183,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 });
});
};
@ -226,18 +239,28 @@ 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 onRoomAccessRadioToggle = (roomAccess) => {
if (
@ -264,12 +287,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
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 });
});
};
@ -402,8 +425,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
render() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isEncrypted = this.state.encrypted;

View file

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

View file

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

View file

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

View file

@ -15,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,9 +29,11 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import { copyPlaintext } from "../../../../../utils/strings";
import * as ContextMenu from "../../../../structures/ContextMenu";
import { toRightOf } from "../../../../structures/ContextMenu";
import BugReportDialog from '../../../dialogs/BugReportDialog';
import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu";
interface IProps {
closeSettingsFn: () => {};
closeSettingsFn: () => void;
}
interface IState {
@ -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, {});
};
@ -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;

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";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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