Merge branch 'develop' into string-pl

pull/21833/head
Aaron Raimist 2021-06-23 17:54:42 -05:00
commit 7b7ad76fe4
25 changed files with 316 additions and 195 deletions

View File

@ -79,7 +79,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "12.0.0", "matrix-js-sdk": "12.0.0",
"matrix-widget-api": "^0.1.0-beta.14", "matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
@ -123,7 +123,7 @@
"@sinonjs/fake-timers": "^7.0.2", "@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11", "@types/classnames": "^2.2.11",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/diff-match-patch": "^1.0.5", "@types/diff-match-patch": "^1.0.32",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3", "@types/linkifyjs": "^2.1.3",

View File

@ -71,7 +71,7 @@ limitations under the License.
&::before { &::before {
background-color: #ffffff; background-color: #ffffff;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-size: 90%; mask-size: 80%;
} }
&::after { &::after {

View File

@ -21,7 +21,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
&.mx_cryptoEvent_icon::after { &.mx_cryptoEvent_icon::after {

View File

@ -45,7 +45,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
// transparent-looking border surrounding the shield for when overlain over avatars // transparent-looking border surrounding the shield for when overlain over avatars
@ -59,7 +59,7 @@ limitations under the License.
} }
// shrink the infill of the badge // shrink the infill of the badge
&::before { &::before {
mask-size: 65%; mask-size: 60%;
} }
} }

View File

@ -345,7 +345,7 @@ $hover-select-border: 4px;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
} }

View File

@ -23,7 +23,7 @@ limitations under the License.
.mx_DialPad_button { .mx_DialPad_button {
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: $theme-button-bg-color; background-color: $dialpad-button-bg-color;
border-radius: 40px; border-radius: 40px;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;

View File

@ -27,9 +27,22 @@ limitations under the License.
} }
.mx_DialPadContextMenu_dialled { .mx_DialPadContextMenu_dialled {
height: 1em; height: 1.5em;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
max-width: 150px;
border: none;
margin: 0px;
}
.mx_DialPadContextMenu_dialled input {
font-size: 18px;
font-weight: 600;
overflow: hidden;
max-width: 150px;
text-align: left;
direction: rtl;
padding: 8px 0px;
background-color: rgb(0, 0, 0, 0);
} }
.mx_DialPadContextMenu_dialPad { .mx_DialPadContextMenu_dialPad {

View File

@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $bg-color; $roomlist-filter-active-bg-color: $bg-color;

View File

@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View File

@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View File

@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: #ffffff; $roomlist-filter-active-bg-color: #ffffff;

View File

@ -1,85 +0,0 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
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 SettingsStore from "./settings/SettingsStore";
import {SettingLevel} from "./settings/SettingLevel";
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
export default {
hasAnyLabeledDevices: async function() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some(d => !!d.label);
},
getDevices: function() {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
const audiooutput = [];
const audioinput = [];
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;
}
});
// console.log("Loaded WebRTC Devices", mediaDevices);
return {
audiooutput,
audioinput,
videoinput,
};
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
},
loadDevices: function() {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
},
setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
},
setAudioInput: function(deviceId) {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallAudioInput(deviceId);
},
setVideoInput: function(deviceId) {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallVideoInput(deviceId);
},
getAudioOutput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
},
getAudioInput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
},
getVideoInput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
},
};

120
src/MediaDeviceHandler.ts Normal file
View File

@ -0,0 +1,120 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 SettingsStore from "./settings/SettingsStore";
import { SettingLevel } from "./settings/SettingLevel";
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
import EventEmitter from 'events';
interface IMediaDevices {
audioOutput: Array<MediaDeviceInfo>;
audioInput: Array<MediaDeviceInfo>;
videoInput: Array<MediaDeviceInfo>;
}
export enum MediaDeviceHandlerEvent {
AudioOutputChanged = "audio_output_changed",
}
export default class MediaDeviceHandler extends EventEmitter {
private static internalInstance;
public static get instance(): MediaDeviceHandler {
if (!MediaDeviceHandler.internalInstance) {
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
}
return MediaDeviceHandler.internalInstance;
}
public static async hasAnyLabeledDevices(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some(d => Boolean(d.label));
}
public static async getDevices(): Promise<IMediaDevices> {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioOutput = [];
const audioInput = [];
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) {
console.warn('Unable to refresh WebRTC Devices: ', error);
}
}
/**
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
*/
public static loadDevices(): void {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
}
public setAudioOutput(deviceId: string): void {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
}
/**
* This will not change the device that a potential call uses. The call will
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
public setAudioInput(deviceId: string): void {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallAudioInput(deviceId);
}
/**
* This will not change the device that a potential call uses. The call will
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
public setVideoInput(deviceId: string): void {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallVideoInput(deviceId);
}
public static getAudioOutput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
}
public static getAudioInput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
}
public static getVideoInput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
}
}

View File

@ -22,7 +22,7 @@ import { MatrixClient } from 'matrix-js-sdk/src/client';
import {Key} from '../../Keyboard'; import {Key} from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import MediaDeviceHandler from '../../MediaDeviceHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
@ -167,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
CallMediaHandler.loadDevices(); MediaDeviceHandler.loadDevices();
fixupColorFonts(); fixupColorFonts();

View File

@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} }
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) { if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault(); ev.preventDefault();
this.removeFromDirectory(room); this.removeFromDirectory(room);
} else {
this.showRoom(room);
} }
}; };
@ -568,11 +567,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
let avatarUrl = null; let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [ return [
<div key={ `${room.room_id}_avatar` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_avatar` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomAvatar" className="mx_RoomDirectory_roomAvatar"
> >
<BaseAvatar <BaseAvatar
@ -584,42 +583,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
url={avatarUrl} url={avatarUrl}
/> />
</div>, </div>,
<div key={ `${room.room_id}_description` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_description` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomDescription" className="mx_RoomDirectory_roomDescription"
> >
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp; <div
<div className="mx_RoomDirectory_topic" className="mx_RoomDirectory_name"
onClick={ (ev) => { ev.stopPropagation(); } } onMouseDown={(ev) => this.onRoomClicked(room, ev)}
>
{ name }
</div>&nbsp;
<div
className="mx_RoomDirectory_topic"
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
dangerouslySetInnerHTML={{ __html: topic }} dangerouslySetInnerHTML={{ __html: topic }}
/> />
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div> <div
className="mx_RoomDirectory_alias"
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
>
{ getDisplayAliasForRoom(room) }
</div>
</div>, </div>,
<div key={ `${room.room_id}_memberCount` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_memberCount` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomMemberCount" className="mx_RoomDirectory_roomMemberCount"
> >
{ room.num_joined_members } { room.num_joined_members }
</div>, </div>,
<div key={ `${room.room_id}_preview` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_preview` }
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
// cancel onMouseDown otherwise shift-clicking highlights text // cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_preview" className="mx_RoomDirectory_preview"
> >
{previewButton} { previewButton }
</div>, </div>,
<div key={ `${room.room_id}_join` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_join` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_join" className="mx_RoomDirectory_join"
> >
{joinOrViewButton} { joinOrViewButton }
</div>, </div>,
]; ];
} }

View File

@ -18,6 +18,7 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Field from "../elements/Field";
import Dialpad from '../voip/DialPad'; import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.setState({value: this.state.value + digit}); this.setState({value: this.state.value + digit});
} }
onChange = (ev) => {
this.setState({value: ev.target.value});
}
render() { render() {
return <ContextMenu {...this.props}> return <ContextMenu {...this.props}>
<div className="mx_DialPadContextMenu_header"> <div className="mx_DialPadContextMenu_header">
<div> <div>
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span> <span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
</div> </div>
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div> <Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
</div> </div>
<div className="mx_DialPadContextMenu_horizSep" /> <div className="mx_DialPadContextMenu_horizSep" />
<div className="mx_DialPadContextMenu_dialPad"> <div className="mx_DialPadContextMenu_dialPad">

View File

@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
setSelectedToAdd(new Set(selectedToAdd)); setSelectedToAdd(new Set(selectedToAdd));
} : null; } : null;
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
);
}
return <div className="mx_AddExistingToSpace"> return <div className="mx_AddExistingToSpace">
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section"> <div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3> <h3>{ _t("Rooms") }</h3>
{ rooms.map(room => { <TruncatedList
return <Entry truncateAt={truncateAt}
key={room.roomId} createOverflowElement={overflowTile}
room={room} getChildren={(start, end) => rooms.slice(start, end).map(room =>
checked={selectedToAdd.has(room)} <Entry
onChange={onChange ? (checked) => { key={room.roomId}
onChange(checked, room); room={room}
} : null} checked={selectedToAdd.has(room)}
/>; onChange={onChange ? (checked) => {
}) } onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div> </div>
) : undefined } ) : undefined }

View File

@ -40,6 +40,9 @@ import NotificationBadge from "../rooms/NotificationBadge";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
const AVATAR_SIZE = 30; const AVATAR_SIZE = 30;
@ -196,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}).match(lcQuery); }).match(lcQuery);
} }
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
);
}
return <BaseDialog return <BaseDialog
title={_t("Forward message")} title={_t("Forward message")}
className="mx_ForwardDialog" className="mx_ForwardDialog"
@ -228,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
<AutoHideScrollbar className="mx_ForwardList_content"> <AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_ForwardList_results"> <div className="mx_ForwardList_results">
{ rooms.map(room => <TruncatedList
<Entry truncateAt={truncateAt}
key={room.roomId} createOverflowElement={overflowTile}
room={room} getChildren={(start, end) => rooms.slice(start, end).map(room =>
event={event} <Entry
matrixClient={cli} key={room.roomId}
onFinished={onFinished} room={room}
/>, event={event}
) } matrixClient={cli}
onFinished={onFinished}
/>,
)}
getChildCount={() => rooms.length}
/>
</div> </div>
) : <span className="mx_ForwardList_noResults"> ) : <span className="mx_ForwardList_noResults">
{ _t("No results") } { _t("No results") }

View File

@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef(); private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
state: IState = { state: IState = {
disabledButtonIds: [], disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
.map(b => b.id),
}; };
constructor(props) { constructor(props) {

View File

@ -16,31 +16,29 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TruncatedList") interface IProps {
export default class TruncatedList extends React.Component { // The number of elements to show before truncating. If negative, no truncation is done.
static propTypes = { truncateAt?: number;
// The number of elements to show before truncating. If negative, no truncation is done. // The className to apply to the wrapping div
truncateAt: PropTypes.number, className?: string;
// The className to apply to the wrapping div // A function that returns the children to be rendered into the element.
className: PropTypes.string, // The start element is included, the end is not (as in `slice`).
// A function that returns the children to be rendered into the element. // If omitted, the React child elements will be used. This parameter can be used
// function getChildren(start: number, end: number): Array<React.Node> // to avoid creating unnecessary React elements.
// The start element is included, the end is not (as in `slice`). getChildren?: (start: number, end: number) => Array<React.ReactNode>;
// If omitted, the React child elements will be used. This parameter can be used // A function that should return the total number of child element available.
// to avoid creating unnecessary React elements. // Required if getChildren is supplied.
getChildren: PropTypes.func, getChildCount?: () => number;
// A function that should return the total number of child element available. // A function which will be invoked when an overflow element is required.
// Required if getChildren is supplied. // This will be inserted after the children.
getChildCount: PropTypes.func, createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
// A function which will be invoked when an overflow element is required. }
// This will be inserted after the children.
createOverflowElement: PropTypes.func,
};
@replaceableComponent("views.elements.TruncatedList")
export default class TruncatedList extends React.Component<IProps> {
static defaultProps ={ static defaultProps ={
truncateAt: 2, truncateAt: 2,
createOverflowElement(overflowCount, totalCount) { createOverflowElement(overflowCount, totalCount) {
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
}, },
}; };
_getChildren(start, end) { private getChildren(start: number, end: number): Array<React.ReactNode> {
if (this.props.getChildren && this.props.getChildCount) { if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildren(start, end); return this.props.getChildren(start, end);
} else { } else {
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
} }
} }
_getChildCount() { private getChildCount(): number {
if (this.props.getChildren && this.props.getChildCount) { if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildCount(); return this.props.getChildCount();
} else { } else {
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
} }
} }
render() { public render() {
let overflowNode = null; let overflowNode = null;
const totalChildren = this._getChildCount(); const totalChildren = this.getChildCount();
let upperBound = totalChildren; let upperBound = totalChildren;
if (this.props.truncateAt >= 0) { if (this.props.truncateAt >= 0) {
const overflowCount = totalChildren - this.props.truncateAt; const overflowCount = totalChildren - this.props.truncateAt;
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
upperBound = this.props.truncateAt; upperBound = this.props.truncateAt;
} }
} }
const childNodes = this._getChildren(0, upperBound); const childNodes = this.getChildren(0, upperBound);
return ( return (
<div className={this.props.className}> <div className={this.props.className}>

View File

@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_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 CallMediaHandler from "../../../CallMediaHandler"; import MediaDeviceHandler from "../../../MediaDeviceHandler";
interface IProps { interface IProps {
room: Room; room: Room;
@ -129,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might // Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
// 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 CallMediaHandler.getDevices(); const devices = await MediaDeviceHandler.getDevices();
if (!devices?.['audioinput']?.length) { if (!devices?.['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,7 +18,7 @@ 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 CallMediaHandler from "../../../../../CallMediaHandler"; import MediaDeviceHandler 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";
@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices(); const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) { if (canSeeDeviceLabels) {
this._refreshMediaDevices(); this._refreshMediaDevices();
} }
@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
_refreshMediaDevices = async (stream) => { _refreshMediaDevices = async (stream) => {
this.setState({ this.setState({
mediaDevices: await CallMediaHandler.getDevices(), mediaDevices: await MediaDeviceHandler.getDevices(),
activeAudioOutput: CallMediaHandler.getAudioOutput(), activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
activeAudioInput: CallMediaHandler.getAudioInput(), activeAudioInput: MediaDeviceHandler.getAudioInput(),
activeVideoInput: CallMediaHandler.getVideoInput(), activeVideoInput: 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)
@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
}; };
_setAudioOutput = (e) => { _setAudioOutput = (e) => {
CallMediaHandler.setAudioOutput(e.target.value); MediaDeviceHandler.instance.setAudioOutput(e.target.value);
this.setState({ this.setState({
activeAudioOutput: e.target.value, activeAudioOutput: e.target.value,
}); });
}; };
_setAudioInput = (e) => { _setAudioInput = (e) => {
CallMediaHandler.setAudioInput(e.target.value); MediaDeviceHandler.instance.setAudioInput(e.target.value);
this.setState({ this.setState({
activeAudioInput: e.target.value, activeAudioInput: e.target.value,
}); });
}; };
_setVideoInput = (e) => { _setVideoInput = (e) => {
CallMediaHandler.setVideoInput(e.target.value); MediaDeviceHandler.instance.setVideoInput(e.target.value);
this.setState({ this.setState({
activeVideoInput: e.target.value, activeVideoInput: e.target.value,
}); });
@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
} }
}; };
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
if (audioOutputs.length > 0) { if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs); const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = ( speakerDropdown = (
@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
); );
} }
const audioInputs = this.state.mediaDevices.audioinput.slice(0); const audioInputs = this.state.mediaDevices.audioInput.slice(0);
if (audioInputs.length > 0) { if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs); const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = ( microphoneDropdown = (
@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
); );
} }
const videoInputs = this.state.mediaDevices.videoinput.slice(0); const videoInputs = this.state.mediaDevices.videoInput.slice(0);
if (videoInputs.length > 0) { if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs); const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = ( webcamDropdown = (

View File

@ -17,7 +17,7 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from 'matrix-js-sdk/src/logger';
import CallMediaHandler from "../../../CallMediaHandler"; import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
interface IProps { interface IProps {
feed: CallFeed, feed: CallFeed,
@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component<IProps> {
private element = createRef<HTMLAudioElement>(); private element = createRef<HTMLAudioElement>();
componentDidMount() { componentDidMount() {
MediaDeviceHandler.instance.addListener(
MediaDeviceHandlerEvent.AudioOutputChanged,
this.onAudioOutputChanged,
);
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.playMedia(); this.playMedia();
} }
componentWillUnmount() { componentWillUnmount() {
MediaDeviceHandler.instance.removeListener(
MediaDeviceHandlerEvent.AudioOutputChanged,
this.onAudioOutputChanged,
);
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
this.stopMedia(); this.stopMedia();
} }
private playMedia() { private onAudioOutputChanged = (audioOutput: string) => {
const element = this.element.current; const element = this.element.current;
const audioOutput = CallMediaHandler.getAudioOutput();
if (audioOutput) { if (audioOutput) {
try { try {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component<IProps> {
logger.warn("Couldn't set requested audio output device: using default", e); logger.warn("Couldn't set requested audio output device: using default", e);
} }
} }
}
private playMedia() {
const element = this.element.current;
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
element.muted = false; element.muted = false;
element.srcObject = this.props.feed.stream; element.srcObject = this.props.feed.stream;
element.autoplay = true; element.autoplay = true;

View File

@ -17,7 +17,7 @@ limitations under the License.
import * as Recorder from 'opus-recorder'; import * as Recorder from 'opus-recorder';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js'; import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
import CallMediaHandler from "../CallMediaHandler"; import MediaDeviceHandler from "../MediaDeviceHandler";
import {SimpleObservable} from "matrix-widget-api"; import {SimpleObservable} from "matrix-widget-api";
import {clamp, percentageOf, percentageWithin} from "../utils/numbers"; import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
import EventEmitter from "events"; import EventEmitter from "events";
@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
audio: { audio: {
channelCount: CHANNELS, channelCount: CHANNELS,
noiseSuppression: true, // browsers ignore constraints they can't honour noiseSuppression: true, // browsers ignore constraints they can't honour
deviceId: CallMediaHandler.getAudioInput(), deviceId: MediaDeviceHandler.getAudioInput(),
}, },
}); });
this.recorderContext = createAudioContext({ this.recorderContext = createAudioContext({

View File

@ -1334,6 +1334,7 @@
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
version "3.2.3" version "3.2.3"
uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
@ -5772,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17" "@babel/traverse" "^7.13.17"
walk "^2.3.14" walk "^2.3.14"
matrix-widget-api@^0.1.0-beta.14: matrix-widget-api@^0.1.0-beta.15:
version "0.1.0-beta.14" version "0.1.0-beta.15"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"