Implement voice broadcast device selection (#9572)
This commit is contained in:
parent
272aae0973
commit
436146105e
15 changed files with 248 additions and 51 deletions
|
@ -27,5 +27,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Icon_16 {
|
.mx_Icon_16 {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
flex: 0 0 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,3 +50,7 @@ limitations under the License.
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_VoiceBroadcastHeader_mic--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
||||||
export enum MediaDeviceKindEnum {
|
export enum MediaDeviceKindEnum {
|
||||||
|
@ -79,6 +80,18 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>): string => {
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -472,6 +472,35 @@ export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">
|
||||||
return { left, top, chevronOffset };
|
return { left, top, chevronOffset };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ToLeftOf = {
|
||||||
|
chevronOffset: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Placement method for <ContextMenu /> to position context menu to left of elementRect with chevronOffset
|
||||||
|
export const toLeftOf = (elementRect: DOMRect, chevronOffset = 12): ToLeftOf => {
|
||||||
|
const right = UIStore.instance.windowWidth - elementRect.left + window.scrollX - 3;
|
||||||
|
let top = elementRect.top + (elementRect.height / 2) + window.scrollY;
|
||||||
|
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||||
|
return { right, top, chevronOffset };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placement method for <ContextMenu /> to position context menu of or right of elementRect
|
||||||
|
* depending on which side has more space.
|
||||||
|
*/
|
||||||
|
export const toLeftOrRightOf = (elementRect: DOMRect, chevronOffset = 12): ToRightOf | ToLeftOf => {
|
||||||
|
const spaceToTheLeft = elementRect.left;
|
||||||
|
const spaceToTheRight = UIStore.instance.windowWidth - elementRect.right;
|
||||||
|
|
||||||
|
if (spaceToTheLeft > spaceToTheRight) {
|
||||||
|
return toLeftOf(elementRect, chevronOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toRightOf(elementRect, chevronOffset);
|
||||||
|
};
|
||||||
|
|
||||||
export type AboveLeftOf = IPosition & {
|
export type AboveLeftOf = IPosition & {
|
||||||
chevronFace: ChevronFace;
|
chevronFace: ChevronFace;
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRadioProps extends React.ComponentProps<typeof MenuItemRadio> {
|
interface IRadioProps extends React.ComponentProps<typeof MenuItemRadio> {
|
||||||
iconClassName: string;
|
iconClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
|
export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
|
||||||
|
@ -67,7 +67,7 @@ export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
|
||||||
active={active}
|
active={active}
|
||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
||||||
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||||
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
|
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
|
||||||
</MenuItemRadio>;
|
</MenuItemRadio>;
|
||||||
|
|
|
@ -27,18 +27,6 @@ import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import { requestMediaPermissions } from '../../../../../utils/media/requestMediaPermissions';
|
import { requestMediaPermissions } from '../../../../../utils/media/requestMediaPermissions';
|
||||||
|
|
||||||
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 {
|
interface IState {
|
||||||
mediaDevices: IMediaDevices;
|
mediaDevices: IMediaDevices;
|
||||||
[MediaDeviceKindEnum.AudioOutput]: string;
|
[MediaDeviceKindEnum.AudioOutput]: string;
|
||||||
|
@ -116,7 +104,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
||||||
const devices = this.state.mediaDevices[kind].slice(0);
|
const devices = this.state.mediaDevices[kind].slice(0);
|
||||||
if (devices.length === 0) return null;
|
if (devices.length === 0) return null;
|
||||||
|
|
||||||
const defaultDevice = getDefaultDevice(devices);
|
const defaultDevice = MediaDeviceHandler.getDefaultDevice(devices);
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
element="select"
|
element="select"
|
||||||
|
|
|
@ -107,6 +107,7 @@
|
||||||
"Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others",
|
"Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others",
|
||||||
"Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other",
|
"Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other",
|
||||||
"Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)",
|
"Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)",
|
||||||
|
"Default Device": "Default Device",
|
||||||
"%(name)s is requesting verification": "%(name)s is requesting verification",
|
"%(name)s is requesting verification": "%(name)s is requesting verification",
|
||||||
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings",
|
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings",
|
||||||
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again",
|
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again",
|
||||||
|
@ -1619,7 +1620,6 @@
|
||||||
"Group all your people in one place.": "Group all your people in one place.",
|
"Group all your people in one place.": "Group all your people in one place.",
|
||||||
"Rooms outside of a space": "Rooms outside of a space",
|
"Rooms outside of a space": "Rooms outside of a space",
|
||||||
"Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.",
|
"Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.",
|
||||||
"Default Device": "Default Device",
|
|
||||||
"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",
|
||||||
"Audio Output": "Audio Output",
|
"Audio Output": "Audio Output",
|
||||||
|
|
|
@ -12,7 +12,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { LiveBadge } from "../..";
|
import { LiveBadge } from "../..";
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||||
|
@ -28,8 +29,9 @@ import { formatTimeLeft } from "../../../DateUtils";
|
||||||
interface VoiceBroadcastHeaderProps {
|
interface VoiceBroadcastHeaderProps {
|
||||||
live?: boolean;
|
live?: boolean;
|
||||||
onCloseClick?: () => void;
|
onCloseClick?: () => void;
|
||||||
|
onMicrophoneLineClick?: () => void;
|
||||||
room: Room;
|
room: Room;
|
||||||
sender: RoomMember;
|
microphoneLabel?: string;
|
||||||
showBroadcast?: boolean;
|
showBroadcast?: boolean;
|
||||||
timeLeft?: number;
|
timeLeft?: number;
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
|
@ -38,8 +40,9 @@ interface VoiceBroadcastHeaderProps {
|
||||||
export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
||||||
live = false,
|
live = false,
|
||||||
onCloseClick = () => {},
|
onCloseClick = () => {},
|
||||||
|
onMicrophoneLineClick,
|
||||||
room,
|
room,
|
||||||
sender,
|
microphoneLabel,
|
||||||
showBroadcast = false,
|
showBroadcast = false,
|
||||||
showClose = false,
|
showClose = false,
|
||||||
timeLeft,
|
timeLeft,
|
||||||
|
@ -66,16 +69,28 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
|
||||||
</div>
|
</div>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const microphoneLineClasses = classNames({
|
||||||
|
mx_VoiceBroadcastHeader_line: true,
|
||||||
|
["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick,
|
||||||
|
});
|
||||||
|
|
||||||
|
const microphoneLine = microphoneLabel
|
||||||
|
? <div
|
||||||
|
className={microphoneLineClasses}
|
||||||
|
onClick={onMicrophoneLineClick}
|
||||||
|
>
|
||||||
|
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
|
||||||
|
<span>{ microphoneLabel }</span>
|
||||||
|
</div>
|
||||||
|
: null;
|
||||||
|
|
||||||
return <div className="mx_VoiceBroadcastHeader">
|
return <div className="mx_VoiceBroadcastHeader">
|
||||||
<RoomAvatar room={room} width={32} height={32} />
|
<RoomAvatar room={room} width={32} height={32} />
|
||||||
<div className="mx_VoiceBroadcastHeader_content">
|
<div className="mx_VoiceBroadcastHeader_content">
|
||||||
<div className="mx_VoiceBroadcastHeader_room">
|
<div className="mx_VoiceBroadcastHeader_room">
|
||||||
{ room.name }
|
{ room.name }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_VoiceBroadcastHeader_line">
|
{ microphoneLine }
|
||||||
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
<span>{ sender.name }</span>
|
|
||||||
</div>
|
|
||||||
{ timeLeftLine }
|
{ timeLeftLine }
|
||||||
{ broadcast }
|
{ broadcast }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -80,7 +80,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
||||||
<div className="mx_VoiceBroadcastBody">
|
<div className="mx_VoiceBroadcastBody">
|
||||||
<VoiceBroadcastHeader
|
<VoiceBroadcastHeader
|
||||||
live={live}
|
live={live}
|
||||||
sender={sender}
|
microphoneLabel={sender?.name}
|
||||||
room={room}
|
room={room}
|
||||||
showBroadcast={true}
|
showBroadcast={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,26 +14,106 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
import { VoiceBroadcastHeader } from "../..";
|
import { VoiceBroadcastHeader } from "../..";
|
||||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||||
import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording";
|
import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording";
|
||||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
IconizedContextMenuRadio,
|
||||||
|
} from "../../../components/views/context_menus/IconizedContextMenu";
|
||||||
|
import { requestMediaPermissions } from "../../../utils/media/requestMediaPermissions";
|
||||||
|
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||||
|
import { toLeftOrRightOf } from "../../../components/structures/ContextMenu";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
voiceBroadcastPreRecording: VoiceBroadcastPreRecording;
|
voiceBroadcastPreRecording: VoiceBroadcastPreRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
devices: MediaDeviceInfo[];
|
||||||
|
device: MediaDeviceInfo | null;
|
||||||
|
showDeviceSelect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
|
export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
|
||||||
voiceBroadcastPreRecording,
|
voiceBroadcastPreRecording,
|
||||||
}) => {
|
}) => {
|
||||||
return <div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip">
|
const shouldRequestPermissionsRef = useRef<boolean>(true);
|
||||||
|
const pipRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
devices: [],
|
||||||
|
device: null,
|
||||||
|
showDeviceSelect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldRequestPermissionsRef.current) {
|
||||||
|
shouldRequestPermissionsRef.current = false;
|
||||||
|
requestMediaPermissions(false).then((stream: MediaStream | undefined) => {
|
||||||
|
MediaDeviceHandler.getDevices().then(({ audioinput }) => {
|
||||||
|
MediaDeviceHandler.getDefaultDevice(audioinput);
|
||||||
|
const deviceFromSettings = MediaDeviceHandler.getAudioInput();
|
||||||
|
const device = audioinput.find((d) => {
|
||||||
|
return d.deviceId === deviceFromSettings;
|
||||||
|
}) || audioinput[0];
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
devices: audioinput,
|
||||||
|
device,
|
||||||
|
});
|
||||||
|
stream?.getTracks().forEach(t => t.stop());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeviceOptionClick = (device: MediaDeviceInfo) => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
device,
|
||||||
|
showDeviceSelect: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMicrophoneLineClick = () => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
showDeviceSelect: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deviceOptions = state.devices.map((d: MediaDeviceInfo) => {
|
||||||
|
return <IconizedContextMenuRadio
|
||||||
|
key={d.deviceId}
|
||||||
|
active={d.deviceId === state.device?.deviceId}
|
||||||
|
onClick={() => onDeviceOptionClick(d)}
|
||||||
|
label={d.label}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const devicesMenu = state.showDeviceSelect && pipRef.current
|
||||||
|
? <IconizedContextMenu
|
||||||
|
mountAsChild={false}
|
||||||
|
onFinished={() => {}}
|
||||||
|
{...toLeftOrRightOf(pipRef.current.getBoundingClientRect(), 0)}
|
||||||
|
>
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ deviceOptions }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip"
|
||||||
|
ref={pipRef}
|
||||||
|
>
|
||||||
<VoiceBroadcastHeader
|
<VoiceBroadcastHeader
|
||||||
onCloseClick={voiceBroadcastPreRecording.cancel}
|
onCloseClick={voiceBroadcastPreRecording.cancel}
|
||||||
|
onMicrophoneLineClick={onMicrophoneLineClick}
|
||||||
room={voiceBroadcastPreRecording.room}
|
room={voiceBroadcastPreRecording.room}
|
||||||
sender={voiceBroadcastPreRecording.sender}
|
microphoneLabel={state.device?.label || _t('Default Device')}
|
||||||
showClose={true}
|
showClose={true}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
@ -44,5 +124,6 @@ export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
|
||||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||||
{ _t("Go live") }
|
{ _t("Go live") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
{ devicesMenu }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const VoiceBroadcastRecordingBody: React.FC<VoiceBroadcastRecordingBodyPr
|
||||||
<div className="mx_VoiceBroadcastBody">
|
<div className="mx_VoiceBroadcastBody">
|
||||||
<VoiceBroadcastHeader
|
<VoiceBroadcastHeader
|
||||||
live={live}
|
live={live}
|
||||||
sender={sender}
|
microphoneLabel={sender?.name}
|
||||||
room={room}
|
room={room}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,7 +38,6 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
||||||
timeLeft,
|
timeLeft,
|
||||||
recordingState,
|
recordingState,
|
||||||
room,
|
room,
|
||||||
sender,
|
|
||||||
stopRecording,
|
stopRecording,
|
||||||
toggleRecording,
|
toggleRecording,
|
||||||
} = useVoiceBroadcastRecording(recording);
|
} = useVoiceBroadcastRecording(recording);
|
||||||
|
@ -57,7 +56,6 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
|
||||||
>
|
>
|
||||||
<VoiceBroadcastHeader
|
<VoiceBroadcastHeader
|
||||||
live={live}
|
live={live}
|
||||||
sender={sender}
|
|
||||||
room={room}
|
room={room}
|
||||||
timeLeft={timeLeft}
|
timeLeft={timeLeft}
|
||||||
/>
|
/>
|
||||||
|
|
88
test/components/structures/ContextMenu-test.ts
Normal file
88
test/components/structures/ContextMenu-test.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toLeftOf, toLeftOrRightOf, toRightOf } from "../../../src/components/structures/ContextMenu";
|
||||||
|
import UIStore from "../../../src/stores/UIStore";
|
||||||
|
|
||||||
|
describe("ContextMenu", () => {
|
||||||
|
const rect = new DOMRect();
|
||||||
|
// @ts-ignore
|
||||||
|
rect.left = 23;
|
||||||
|
// @ts-ignore
|
||||||
|
rect.right = 46;
|
||||||
|
// @ts-ignore
|
||||||
|
rect.top = 42;
|
||||||
|
rect.width = 640;
|
||||||
|
rect.height = 480;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.scrollX = 31;
|
||||||
|
window.scrollY = 41;
|
||||||
|
UIStore.instance.windowWidth = 1280;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toLeftOf", () => {
|
||||||
|
it("should return the correct positioning", () => {
|
||||||
|
expect(toLeftOf(rect)).toEqual({
|
||||||
|
chevronOffset: 12,
|
||||||
|
right: 1285, // 1280 - 23 + 31 - 3
|
||||||
|
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toRightOf", () => {
|
||||||
|
it("should return the correct positioning", () => {
|
||||||
|
expect(toRightOf(rect)).toEqual({
|
||||||
|
chevronOffset: 12,
|
||||||
|
left: 80, // 46 + 31 + 3
|
||||||
|
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toLeftOrRightOf", () => {
|
||||||
|
describe("when there is more space to the right", () => {
|
||||||
|
// default case from test setup
|
||||||
|
|
||||||
|
it("should return a position to the right", () => {
|
||||||
|
expect(toLeftOrRightOf(rect)).toEqual({
|
||||||
|
chevronOffset: 12,
|
||||||
|
left: 80, // 46 + 31 + 3
|
||||||
|
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is more space to the left", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
rect.left = 500;
|
||||||
|
// @ts-ignore
|
||||||
|
rect.right = 1000;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a position to the left", () => {
|
||||||
|
expect(toLeftOrRightOf(rect)).toEqual({
|
||||||
|
chevronOffset: 12,
|
||||||
|
right: 808, // 1280 - 500 + 31 - 3
|
||||||
|
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe("VoiceBroadcastHeader", () => {
|
||||||
const renderHeader = (live: boolean, showBroadcast: boolean = undefined): RenderResult => {
|
const renderHeader = (live: boolean, showBroadcast: boolean = undefined): RenderResult => {
|
||||||
return render(<VoiceBroadcastHeader
|
return render(<VoiceBroadcastHeader
|
||||||
live={live}
|
live={live}
|
||||||
sender={sender}
|
microphoneLabel={sender.name}
|
||||||
room={room}
|
room={room}
|
||||||
showBroadcast={showBroadcast}
|
showBroadcast={showBroadcast}
|
||||||
/>);
|
/>);
|
||||||
|
|
|
@ -22,16 +22,6 @@ exports[`VoiceBroadcastRecordingPip when rendering a paused recording should ren
|
||||||
>
|
>
|
||||||
My room
|
My room
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastHeader_line"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Icon mx_Icon_16"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
@userId:matrix.org
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mx_VoiceBroadcastHeader_line"
|
class="mx_VoiceBroadcastHeader_line"
|
||||||
>
|
>
|
||||||
|
@ -107,16 +97,6 @@ exports[`VoiceBroadcastRecordingPip when rendering a started recording should re
|
||||||
>
|
>
|
||||||
My room
|
My room
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastHeader_line"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Icon mx_Icon_16"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
@userId:matrix.org
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mx_VoiceBroadcastHeader_line"
|
class="mx_VoiceBroadcastHeader_line"
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue