Merge remote-tracking branch 'upstream/develop' into fix/17130/draggable-pip

This commit is contained in:
Šimon Brandner 2021-07-09 15:36:14 +02:00
commit 007548aa7f
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
20 changed files with 297 additions and 252 deletions

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr"; import "@types/modernizr";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg"; import { IMatrixClientPeg } from "../MatrixClientPeg";
@ -50,7 +50,6 @@ import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
declare global { declare global {
interface Window { interface Window {
Modernizr: ModernizrStatic;
matrixChat: ReturnType<Renderer>; matrixChat: ReturnType<Renderer>;
mxMatrixClientPeg: IMatrixClientPeg; mxMatrixClientPeg: IMatrixClientPeg;
Olm: { Olm: {

View file

@ -569,7 +569,7 @@ export default class ContentMessages {
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload }); dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
// Focus the composer view // Focus the composer view
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
function onProgress(ev) { function onProgress(ev) {
upload.total = ev.total; upload.total = ev.total;

View file

@ -20,12 +20,15 @@ import { SettingLevel } from "./settings/SettingLevel";
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
import EventEmitter from 'events'; import EventEmitter from 'events';
interface IMediaDevices { // XXX: MediaDeviceKind is a union type, so we make our own enum
audioOutput: Array<MediaDeviceInfo>; export enum MediaDeviceKindEnum {
audioInput: Array<MediaDeviceInfo>; AudioOutput = "audiooutput",
videoInput: Array<MediaDeviceInfo>; AudioInput = "audioinput",
VideoInput = "videoinput",
} }
export type IMediaDevices = Record<MediaDeviceKindEnum, Array<MediaDeviceInfo>>;
export enum MediaDeviceHandlerEvent { export enum MediaDeviceHandlerEvent {
AudioOutputChanged = "audio_output_changed", AudioOutputChanged = "audio_output_changed",
} }
@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
const output = {
[MediaDeviceKindEnum.AudioOutput]: [],
[MediaDeviceKindEnum.AudioInput]: [],
[MediaDeviceKindEnum.VideoInput]: [],
};
const audioOutput = []; devices.forEach((device) => output[device.kind].push(device));
const audioInput = []; return output;
const videoInput = [];
devices.forEach((device) => {
switch (device.kind) {
case 'audiooutput': audioOutput.push(device); break;
case 'audioinput': audioInput.push(device); break;
case 'videoinput': videoInput.push(device); break;
}
});
return { audioOutput, audioInput, videoInput };
} catch (error) { } catch (error) {
console.warn('Unable to refresh WebRTC Devices: ', error); console.warn('Unable to refresh WebRTC Devices: ', error);
} }
@ -106,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter {
setMatrixCallVideoInput(deviceId); setMatrixCallVideoInput(deviceId);
} }
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
switch (kind) {
case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break;
case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break;
case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break;
}
}
public static getAudioOutput(): string { public static getAudioOutput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
} }

View file

@ -398,7 +398,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// refocusing during a paste event will make the // refocusing during a paste event will make the
// paste end up in the newly focused element, // paste end up in the newly focused element,
// so dispatch synchronously before paste happens // so dispatch synchronously before paste happens
dis.fire(Action.FocusComposer, true); dis.fire(Action.FocusSendMessageComposer, true);
} }
}; };
@ -552,7 +552,7 @@ class LoggedInView extends React.Component<IProps, IState> {
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input // synchronous dispatch so we focus before key generates input
dis.fire(Action.FocusComposer, true); dis.fire(Action.FocusSendMessageComposer, true);
ev.stopPropagation(); ev.stopPropagation();
// we should *not* preventDefault() here as // we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer // that would prevent typing in the now-focussed composer

View file

@ -443,7 +443,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
CountlyAnalytics.instance.trackPageChange(durationMs); CountlyAnalytics.instance.trackPageChange(durationMs);
} }
if (this.focusComposer) { if (this.focusComposer) {
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.focusComposer = false; this.focusComposer = false;
} }
} }
@ -1427,7 +1427,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast(false); showNotificationsToast(false);
} }
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.setState({ this.setState({
ready: true, ready: true,
}); });

View file

@ -48,6 +48,9 @@ import { ActionPayload } from "../../dispatcher/payloads";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
function track(action: string) { function track(action: string) {
Analytics.trackEvent('RoomDirectory', action); Analytics.trackEvent('RoomDirectory', action);
} }
@ -61,7 +64,7 @@ interface IState {
loading: boolean; loading: boolean;
protocolsLoading: boolean; protocolsLoading: boolean;
error?: string; error?: string;
instanceId: string | symbol; instanceId: string;
roomServer: string; roomServer: string;
filterString: string; filterString: string;
selectedCommunityId?: string; selectedCommunityId?: string;
@ -116,6 +119,36 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} else if (!selectedCommunityId) { } else if (!selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response; this.protocols = response;
const myHomeserver = MatrixClientPeg.getHomeserverName();
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY);
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY);
let roomServer = myHomeserver;
if (
SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) ||
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
) {
roomServer = lsRoomServer;
}
let instanceId: string = null;
if (roomServer === myHomeserver && (
lsInstanceId === ALL_ROOMS ||
Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId))
)) {
instanceId = lsInstanceId;
}
// Refresh the room list only if validation failed and we had to change these
if (this.state.instanceId !== instanceId || this.state.roomServer !== roomServer) {
this.setState({
protocolsLoading: false,
instanceId,
roomServer,
});
this.refreshRoomList();
return;
}
this.setState({ protocolsLoading: false }); this.setState({ protocolsLoading: false });
}, (err) => { }, (err) => {
console.warn(`error loading third party protocols: ${err}`); console.warn(`error loading third party protocols: ${err}`);
@ -150,8 +183,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
publicRooms: [], publicRooms: [],
loading: true, loading: true,
error: null, error: null,
instanceId: undefined, instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
roomServer: MatrixClientPeg.getHomeserverName(), roomServer: localStorage.getItem(LAST_SERVER_KEY),
filterString: this.props.initialText || "", filterString: this.props.initialText || "",
selectedCommunityId, selectedCommunityId,
communityName: null, communityName: null,
@ -342,7 +375,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} }
}; };
private onOptionChange = (server: string, instanceId?: string | symbol) => { private onOptionChange = (server: string, instanceId?: string) => {
// clear next batch so we don't try to load more rooms // clear next batch so we don't try to load more rooms
this.nextBatch = null; this.nextBatch = null;
this.setState({ this.setState({
@ -360,6 +393,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// find the five gitter ones, at which point we do not want // find the five gitter ones, at which point we do not want
// to render all those rooms when switching back to 'all networks'. // to render all those rooms when switching back to 'all networks'.
// Easiest to just blow away the state & re-fetch. // Easiest to just blow away the state & re-fetch.
// We have to be careful here so that we don't set instanceId = "undefined"
localStorage.setItem(LAST_SERVER_KEY, server);
if (instanceId) {
localStorage.setItem(LAST_INSTANCE_KEY, instanceId);
} else {
localStorage.removeItem(LAST_INSTANCE_KEY);
}
}; };
private onFillRequest = (backwards: boolean) => { private onFillRequest = (backwards: boolean) => {

View file

@ -131,7 +131,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
switch (action) { switch (action) {
case RoomListAction.ClearSearch: case RoomListAction.ClearSearch:
this.clearInput(); this.clearInput();
defaultDispatcher.fire(Action.FocusComposer); defaultDispatcher.fire(Action.FocusSendMessageComposer);
break; break;
case RoomListAction.NextRoom: case RoomListAction.NextRoom:
case RoomListAction.PrevRoom: case RoomListAction.PrevRoom:

View file

@ -118,12 +118,12 @@ export default class RoomStatusBar extends React.PureComponent {
this.setState({ isResending: false }); this.setState({ isResending: false });
}); });
this.setState({ isResending: true }); this.setState({ isResending: true });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
_onCancelAllClick = () => { _onCancelAllClick = () => {
Resend.cancelUnsentEvents(this.props.room); Resend.cancelUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {

View file

@ -818,17 +818,16 @@ export default class RoomView extends React.Component<IProps, IState> {
case Action.ComposerInsert: { case Action.ComposerInsert: {
// re-dispatch to the correct composer // re-dispatch to the correct composer
if (this.state.editState) { dis.dispatch({
dis.dispatch({ ...payload,
...payload, action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
action: "edit_composer_insert", });
}); break;
} else { }
dis.dispatch({
...payload, case Action.FocusAComposer: {
action: "send_composer_insert", // re-dispatch to the correct composer
}); dis.fire(this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer);
}
break; break;
} }
@ -1246,7 +1245,7 @@ export default class RoomView extends React.Component<IProps, IState> {
ContentMessages.sharedInstance().sendContentListToRoom( ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, this.context, ev.dataTransfer.files, this.state.room.roomId, this.context,
); );
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.setState({ this.setState({
draggingFile: false, draggingFile: false,
@ -1548,7 +1547,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} else { } else {
// Otherwise we have to jump manually // Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline(); this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
} }
}; };

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,12 +16,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventStatus } from 'matrix-js-sdk/src/models/event'; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils'; import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog"; import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import ReportEventDialog from '../dialogs/ReportEventDialog';
import ViewSource from '../../structures/ViewSource';
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
import ErrorDialog from '../dialogs/ErrorDialog';
import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
export function canCancel(eventStatus) { export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
} }
interface IEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}
interface IProps {
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps?: IEventTileOps;
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread?(): void;
/* callback called when the menu is dismissed */
onFinished(): void;
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog?(): void;
}
interface IState {
canRedact: boolean;
canPin: boolean;
}
@replaceableComponent("views.context_menus.MessageContextMenu") @replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component { export default class MessageContextMenu extends React.Component<IProps, IState> {
static propTypes = {
/* the MatrixEvent associated with the context menu */
mxEvent: PropTypes.object.isRequired,
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps: PropTypes.object,
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread: PropTypes.func,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog: PropTypes.func,
};
state = { state = {
canRedact: false, canRedact: false,
canPin: false, canPin: false,
}; };
componentDidMount() { componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
this._checkPermissions(); this.checkPermissions();
} }
componentWillUnmount() { componentWillUnmount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions); cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
} }
} }
_checkPermissions = () => { private checkPermissions = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
this.setState({ canRedact, canPin }); this.setState({ canRedact, canPin });
}; };
_isPinned() { private isPinned(): boolean {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
if (!pinnedEvent) return false; if (!pinnedEvent) return false;
@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component {
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
} }
onResendReactionsClick = () => { private onResendReactionsClick = (): void => {
for (const reaction of this._getUnsentReactions()) { for (const reaction of this.getUnsentReactions()) {
Resend.resend(reaction); Resend.resend(reaction);
} }
this.closeMenu(); this.closeMenu();
}; };
onReportEventClick = () => { private onReportEventClick = (): void => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent'); }, 'mx_Dialog_reportEvent');
this.closeMenu(); this.closeMenu();
}; };
onViewSourceClick = () => { private onViewSourceClick = (): void => {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, { Modal.createTrackedDialog('View Event Source', '', ViewSource, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
this.closeMenu(); this.closeMenu();
}; };
onRedactClick = () => { private onRedactClick = (): void => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed, reason) => { onFinished: async (proceed: boolean, reason?: string) => {
if (!proceed) return; if (!proceed) return;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
if (this.props.onCloseDialog) this.props.onCloseDialog(); this.props.onCloseDialog?.();
await cli.redactEvent( await cli.redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getRoomId(),
this.props.mxEvent.getId(), this.props.mxEvent.getId(),
@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component {
// (e.g. no errcode or statusCode) as in that case the redactions end up in the // (e.g. no errcode or statusCode) as in that case the redactions end up in the
// detached queue and we show the room status bar to allow retry // detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") { if (typeof code !== "undefined") {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this. // display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onForwardClick = () => { private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent, event: this.props.mxEvent,
@ -167,12 +174,12 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPinClick = () => { private onPinClick = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
if (pinnedIds.includes(eventId)) { if (pinnedIds.includes(eventId)) {
pinnedIds.splice(pinnedIds.indexOf(eventId), 1); pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
} else { } else {
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
closeMenu = () => { private closeMenu = (): void => {
if (this.props.onFinished) this.props.onFinished(); this.props.onFinished();
}; };
onUnhidePreviewClick = () => { private onUnhidePreviewClick = (): void => {
if (this.props.eventTileOps) { this.props.eventTileOps?.unhideWidget();
this.props.eventTileOps.unhideWidget();
}
this.closeMenu(); this.closeMenu();
}; };
onQuoteClick = () => { private onQuoteClick = (): void => {
dis.dispatch({ dis.dispatch({
action: Action.ComposerInsert, action: Action.ComposerInsert,
event: this.props.mxEvent, event: this.props.mxEvent,
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPermalinkClick = (e) => { private onPermalinkClick = (e: React.MouseEvent): void => {
e.preventDefault(); e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent, target: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator,
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onCollapseReplyThreadClick = () => { private onCollapseReplyThreadClick = (): void => {
this.props.collapseReplyThread(); this.props.collapseReplyThread();
this.closeMenu(); this.closeMenu();
}; };
_getReactions(filter) { private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
return room.getPendingEvents().filter(e => { return room.getPendingEvents().filter(e => {
const relation = e.getRelation(); const relation = e.getRelation();
return relation && return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
relation.rel_type === "m.annotation" &&
relation.event_id === eventId &&
filter(e);
}); });
} }
_getPendingReactions() { private getPendingReactions(): MatrixEvent[] {
return this._getReactions(e => canCancel(e.status)); return this.getReactions(e => canCancel(e.status));
} }
_getUnsentReactions() { private getUnsentReactions(): MatrixEvent[] {
return this._getReactions(e => e.status === EventStatus.NOT_SENT); return this.getReactions(e => e.status === EventStatus.NOT_SENT);
} }
render() { render() {
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
const me = cli.getUserId(); const me = cli.getUserId();
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status; const eventStatus = mxEvent.status;
const unsentReactionsCount = this._getUnsentReactions().length; const unsentReactionsCount = this.getUnsentReactions().length;
let resendReactionsButton;
let redactButton; let resendReactionsButton: JSX.Element;
let forwardButton; let redactButton: JSX.Element;
let pinButton; let forwardButton: JSX.Element;
let unhidePreviewButton; let pinButton: JSX.Element;
let externalURLButton; let unhidePreviewButton: JSX.Element;
let quoteButton; let externalURLButton: JSX.Element;
let collapseReplyThread; let quoteButton: JSX.Element;
let redactItemList; let collapseReplyThread: JSX.Element;
let redactItemList: JSX.Element;
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT; const isSent = !eventStatus || eventStatus === EventStatus.SENT;
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
pinButton = ( pinButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin" iconClassName="mx_MessageContextMenu_iconPin"
label={ this._isPinned() ? _t('Unpin') : _t('Pin') } label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
onClick={this.onPinClick} onClick={this.onPinClick}
/> />
); );
@ -333,9 +335,14 @@ export default class MessageContextMenu extends React.Component {
onClick={this.onPermalinkClick} onClick={this.onPermalinkClick}
label= {_t('Share')} label= {_t('Share')}
element="a" element="a"
href={permalink} {
target="_blank" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
rel="noreferrer noopener" ...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
}
}
/> />
); );
@ -350,8 +357,8 @@ export default class MessageContextMenu extends React.Component {
} }
// Bridges can provide a 'external_url' to link back to the source. // Bridges can provide a 'external_url' to link back to the source.
if (typeof (mxEvent.event.content.external_url) === "string" && if (typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.event.content.external_url) isUrlPermitted(mxEvent.getContent().external_url)
) { ) {
externalURLButton = ( externalURLButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -359,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
onClick={this.closeMenu} onClick={this.closeMenu}
label={ _t('Source URL') } label={ _t('Source URL') }
element="a" element="a"
target="_blank" {
rel="noreferrer noopener" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
href={mxEvent.event.content.external_url} ...{
target: "_blank",
rel: "noreferrer noopener",
href: mxEvent.getContent().external_url,
}
}
/> />
); );
} }
@ -376,7 +388,7 @@ export default class MessageContextMenu extends React.Component {
); );
} }
let reportEventButton; let reportEventButton: JSX.Element;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
<IconizedContextMenuOption <IconizedContextMenuOption

View file

@ -41,7 +41,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import { compare } from "../../../utils/strings"; import { compare } from "../../../utils/strings";
export const ALL_ROOMS = Symbol("ALL_ROOMS"); // XXX: We would ideally use a symbol here but we can't since we save this value to localStorage
export const ALL_ROOMS = "ALL_ROOMS";
const SETTING_NAME = "room_directory_servers"; const SETTING_NAME = "room_directory_servers";
@ -94,8 +95,7 @@ export interface IInstance {
fields: object; fields: object;
network_id: string; network_id: string;
// XXX: this is undocumented but we rely on it. // XXX: this is undocumented but we rely on it.
// we inject a fake entry with a symbolic instance_id. instance_id: string;
instance_id: string | symbol;
} }
export interface IProtocol { export interface IProtocol {
@ -112,8 +112,8 @@ export type Protocols = Record<string, IProtocol>;
interface IProps { interface IProps {
protocols: Protocols; protocols: Protocols;
selectedServerName: string; selectedServerName: string;
selectedInstanceId: string | symbol; selectedInstanceId: string;
onOptionChange(server: string, instanceId?: string | symbol): void; onOptionChange(server: string, instanceId?: string): void;
} }
// This dropdown sources homeservers from three places: // This dropdown sources homeservers from three places:
@ -171,7 +171,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
const protocolsList = server === hsName ? Object.values(protocols) : []; const protocolsList = server === hsName ? Object.values(protocols) : [];
if (protocolsList.length > 0) { if (protocolsList.length > 0) {
// add a fake protocol with the ALL_ROOMS symbol // add a fake protocol with ALL_ROOMS
protocolsList.push({ protocolsList.push({
instances: [{ instances: [{
fields: [], fields: [],

View file

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactHTML } from 'react';
import { Key } from '../../../Keyboard'; import { Key } from '../../../Keyboard';
import classnames from 'classnames'; import classnames from 'classnames';
@ -29,7 +29,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
*/ */
interface IProps extends React.InputHTMLAttributes<Element> { interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>; inputRef?: React.Ref<Element>;
element?: string; element?: keyof ReactHTML;
// The kind of button, similar to how Bootstrap works. // The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options. // See available classes for AccessibleButton for options.
kind?: string; kind?: string;
@ -122,7 +122,7 @@ export default function AccessibleButton({
} }
AccessibleButton.defaultProps = { AccessibleButton.defaultProps = {
element: 'div', element: 'div' as keyof ReactHTML,
role: 'button', role: 'button',
tabIndex: 0, tabIndex: 0,
}; };

View file

@ -334,7 +334,7 @@ export default class ReplyThread extends React.Component {
events, events,
}); });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
} }
render() { render() {

View file

@ -22,6 +22,7 @@ import EmojiPicker from "./EmojiPicker";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Action } from '../../../dispatcher/actions';
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -93,6 +94,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
this.props.mxEvent.getRoomId(), this.props.mxEvent.getRoomId(),
myReactions[reaction], myReactions[reaction],
); );
dis.dispatch({ action: Action.FocusAComposer });
// Tell the emoji picker not to bump this in the more frequently used list. // Tell the emoji picker not to bump this in the more frequently used list.
return false; return false;
} else { } else {
@ -104,6 +106,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
}, },
}); });
dis.dispatch({ action: "message_sent" }); dis.dispatch({ action: "message_sent" });
dis.dispatch({ action: Action.FocusAComposer });
return true; return true;
} }
}; };

View file

@ -181,7 +181,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
} else { } else {
this.clearStoredEditorState(); this.clearStoredEditorState();
dis.dispatch({ action: 'edit_event', event: null }); dis.dispatch({ action: 'edit_event', event: null });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
} }
event.preventDefault(); event.preventDefault();
break; break;
@ -200,7 +200,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
private cancelEdit = (): void => { private cancelEdit = (): void => {
this.clearStoredEditorState(); this.clearStoredEditorState();
dis.dispatch({ action: "edit_event", event: null }); dis.dispatch({ action: "edit_event", event: null });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
private get shouldSaveStoredEditorState(): boolean { private get shouldSaveStoredEditorState(): boolean {
@ -375,7 +375,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
// close the event editing and focus composer // close the event editing and focus composer
dis.dispatch({ action: "edit_event", event: null }); dis.dispatch({ action: "edit_event", event: null });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
private cancelPreviousPendingEdit(): void { private cancelPreviousPendingEdit(): void {
@ -452,6 +452,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
} else if (payload.text) { } else if (payload.text) {
this.editorRef.current?.insertPlaintext(payload.text); this.editorRef.current?.insertPlaintext(payload.text);
} }
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
this.editorRef.current.focus();
} }
}; };

View file

@ -497,7 +497,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
switch (payload.action) { switch (payload.action) {
case 'reply_to_event': case 'reply_to_event':
case Action.FocusComposer: case Action.FocusSendMessageComposer:
this.editorRef.current?.focus(); this.editorRef.current?.focus();
break; break;
case "send_composer_insert": case "send_composer_insert":

View file

@ -33,7 +33,7 @@ import RecordingPlayback from "../audio_messages/RecordingPlayback";
import { MsgType } from "matrix-js-sdk/src/@types/event"; import { MsgType } from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import MediaDeviceHandler from "../../../MediaDeviceHandler"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
interface IProps { interface IProps {
room: Room; room: Room;
@ -135,7 +135,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// change between this and recording, but at least we will have tried. // change between this and recording, but at least we will have tried.
try { try {
const devices = await MediaDeviceHandler.getDevices(); const devices = await MediaDeviceHandler.getDevices();
if (!devices?.['audioInput']?.length) { if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, { Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
title: _t("No microphone found"), title: _t("No microphone found"),
description: <> description: <>

View file

@ -18,41 +18,58 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig"; import SdkConfig from "../../../../../SdkConfig";
import MediaDeviceHandler from "../../../../../MediaDeviceHandler"; import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field"; import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent"; 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") @replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
export default class VoiceUserSettingsTab extends React.Component { export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
constructor() { constructor(props: {}) {
super(); super(props);
this.state = { this.state = {
mediaDevices: false, mediaDevices: null,
activeAudioOutput: null, [MediaDeviceKindEnum.AudioOutput]: null,
activeAudioInput: null, [MediaDeviceKindEnum.AudioInput]: null,
activeVideoInput: null, [MediaDeviceKindEnum.VideoInput]: null,
}; };
} }
async componentDidMount() { async componentDidMount() {
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) { if (canSeeDeviceLabels) {
this._refreshMediaDevices(); this.refreshMediaDevices();
} }
} }
_refreshMediaDevices = async (stream) => { private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({ this.setState({
mediaDevices: await MediaDeviceHandler.getDevices(), mediaDevices: await MediaDeviceHandler.getDevices(),
activeAudioOutput: MediaDeviceHandler.getAudioOutput(), [MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
activeAudioInput: MediaDeviceHandler.getAudioInput(), [MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
activeVideoInput: MediaDeviceHandler.getVideoInput(), [MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
}); });
if (stream) { if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
@ -62,7 +79,7 @@ export default class VoiceUserSettingsTab extends React.Component {
} }
}; };
_requestMediaPermissions = async () => { private requestMediaPermissions = async (): Promise<void> => {
let constraints; let constraints;
let stream; let stream;
let error; let error;
@ -86,7 +103,6 @@ export default class VoiceUserSettingsTab extends React.Component {
if (error) { if (error) {
console.log("Failed to list userMedia devices", error); console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'), title: _t('No media permissions'),
description: _t( description: _t(
@ -95,137 +111,93 @@ export default class VoiceUserSettingsTab extends React.Component {
), ),
}); });
} else { } else {
this._refreshMediaDevices(stream); this.refreshMediaDevices(stream);
} }
}; };
_setAudioOutput = (e) => { private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
MediaDeviceHandler.instance.setAudioOutput(e.target.value); MediaDeviceHandler.instance.setDevice(deviceId, kind);
this.setState({ this.setState<null>({ [kind]: deviceId });
activeAudioOutput: e.target.value,
});
}; };
_setAudioInput = (e) => { private changeWebRtcMethod = (p2p: boolean): void => {
MediaDeviceHandler.instance.setAudioInput(e.target.value);
this.setState({
activeAudioInput: e.target.value,
});
};
_setVideoInput = (e) => {
MediaDeviceHandler.instance.setVideoInput(e.target.value);
this.setState({
activeVideoInput: e.target.value,
});
};
_changeWebRtcMethod = (p2p) => {
MatrixClientPeg.get().setForceTURN(!p2p); MatrixClientPeg.get().setForceTURN(!p2p);
}; };
_changeFallbackICEServerAllowed = (allow) => { private changeFallbackICEServerAllowed = (allow: boolean): void => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow); MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
}; };
_renderDeviceOptions(devices, category) { private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
return devices.map((d) => { return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>); return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
}); });
} }
render() { private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); 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 requestButton = null;
let speakerDropdown = null; let speakerDropdown = null;
let microphoneDropdown = null; let microphoneDropdown = null;
let webcamDropdown = null; let webcamDropdown = null;
if (this.state.mediaDevices === false) { if (!this.state.mediaDevices) {
requestButton = ( requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'> <div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p> <p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary"> <AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{_t("Request media permissions")} {_t("Request media permissions")}
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
} else if (this.state.mediaDevices) { } else if (this.state.mediaDevices) {
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>; speakerDropdown = (
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>; this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
webcamDropdown = <p>{ _t('No Webcams detected') }</p>; <p>{ _t('No Audio Outputs detected') }</p>
);
const defaultOption = { microphoneDropdown = (
deviceId: '', this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
label: _t('Default Device'), <p>{ _t('No Microphones detected') }</p>
}; );
const getDefaultDevice = (devices) => { webcamDropdown = (
// Note we're looking for a device with deviceId 'default' but adding a device this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
// with deviceId == the empty string: this is because Chrome gives us a device <p>{ _t('No Webcams detected') }</p>
// 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 ( return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab"> <div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div> <div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{requestButton} { requestButton }
{speakerDropdown} { speakerDropdown }
{microphoneDropdown} { microphoneDropdown }
{webcamDropdown} { webcamDropdown }
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} /> <SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag <SettingsFlag
name='webRtcAllowPeerToPeer' name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE} level={SettingLevel.DEVICE}
onChange={this._changeWebRtcMethod} onChange={this.changeWebRtcMethod}
/> />
<SettingsFlag <SettingsFlag
name='fallbackICEServerAllowed' name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE} level={SettingLevel.DEVICE}
onChange={this._changeFallbackICEServerAllowed} onChange={this.changeFallbackICEServerAllowed}
/> />
</div> </div>
</div> </div>

View file

@ -56,9 +56,21 @@ export enum Action {
CheckUpdates = "check_updates", CheckUpdates = "check_updates",
/** /**
* Focuses the user's cursor to the composer. No additional payload information required. * Focuses the user's cursor to the send message composer. No additional payload information required.
*/ */
FocusComposer = "focus_composer", FocusSendMessageComposer = "focus_send_message_composer",
/**
* Focuses the user's cursor to the edit message composer. No additional payload information required.
*/
FocusEditMessageComposer = "focus_edit_message_composer",
/**
* Focuses the user's cursor to the edit message composer or send message
* composer based on the current edit state. No additional payload
* information required.
*/
FocusAComposer = "focus_a_composer",
/** /**
* Opens the user menu (previously known as the top left menu). No additional payload information required. * Opens the user menu (previously known as the top left menu). No additional payload information required.

View file

@ -1364,17 +1364,17 @@
"Where youre logged in": "Where youre logged in", "Where youre logged in": "Where youre logged in",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.", "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
"A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with", "A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with",
"Default Device": "Default Device",
"No media permissions": "No media permissions", "No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
"Request media permissions": "Request media permissions", "Request media permissions": "Request media permissions",
"No Audio Outputs detected": "No Audio Outputs detected",
"No Microphones detected": "No Microphones detected",
"No Webcams detected": "No Webcams detected",
"Default Device": "Default Device",
"Audio Output": "Audio Output", "Audio Output": "Audio Output",
"No Audio Outputs detected": "No Audio Outputs detected",
"Microphone": "Microphone", "Microphone": "Microphone",
"No Microphones detected": "No Microphones detected",
"Camera": "Camera", "Camera": "Camera",
"No Webcams detected": "No Webcams detected",
"Voice & Video": "Voice & Video", "Voice & Video": "Voice & Video",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"<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>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>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>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.",