Merge branch 'develop' into feat/add-message-edition-wysiwyg-composer
This commit is contained in:
commit
787acbcc31
11 changed files with 273 additions and 105 deletions
|
@ -190,6 +190,9 @@ export default class ScalarAuthClient {
|
||||||
const res = await fetch(scalarRestUrl, {
|
const res = await fetch(scalarRestUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(openidTokenObject),
|
body: JSON.stringify(openidTokenObject),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||||
import { CallStore } from "../../../stores/CallStore";
|
import { CallStore } from "../../../stores/CallStore";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
|
@ -141,36 +141,38 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallBu
|
||||||
}, [videoMuted, setVideoMuted]);
|
}, [videoMuted, setVideoMuted]);
|
||||||
|
|
||||||
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(async () => {
|
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(async () => {
|
||||||
let previewStream: MediaStream;
|
let devices = await MediaDeviceHandler.getDevices();
|
||||||
|
|
||||||
|
// We get the preview stream before requesting devices: this is because
|
||||||
|
// we need (in some browsers) an active media stream in order to get
|
||||||
|
// non-blank labels for the devices.
|
||||||
|
let stream: MediaStream | null = null;
|
||||||
try {
|
try {
|
||||||
// We get the preview stream before requesting devices: this is because
|
if (devices.audioinput.length > 0) {
|
||||||
// we need (in some browsers) an active media stream in order to get
|
// Holding just an audio stream will be enough to get us all device labels, so
|
||||||
// non-blank labels for the devices. According to the docs, we
|
// if video is muted, don't bother requesting video.
|
||||||
// need a stream of each type (audio + video) if we want to enumerate
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
// audio & video devices, although this didn't seem to be the case
|
audio: true,
|
||||||
// in practice for me. We request both anyway.
|
video: !videoMuted && devices.videoinput.length > 0 && { deviceId: videoInputId },
|
||||||
// For similar reasons, we also request a stream even if video is muted,
|
});
|
||||||
// which could be a bit strange but allows us to get the device list
|
} else if (devices.videoinput.length > 0) {
|
||||||
// reliably. One option could be to try & get devices without a stream,
|
// We have to resort to a video stream, even if video is supposed to be muted.
|
||||||
// then try again with a stream if we get blank deviceids, but... ew.
|
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoInputId } });
|
||||||
previewStream = await navigator.mediaDevices.getUserMedia({
|
}
|
||||||
video: { deviceId: videoInputId },
|
|
||||||
audio: { deviceId: MediaDeviceHandler.getAudioInput() },
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to get stream for device ${videoInputId}`, e);
|
logger.error(`Failed to get stream for device ${videoInputId}`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = await MediaDeviceHandler.getDevices();
|
// Refresh the devices now that we hold a stream
|
||||||
|
if (stream !== null) devices = await MediaDeviceHandler.getDevices();
|
||||||
|
|
||||||
// If video is muted, we don't actually want the stream, so we can get rid of
|
// If video is muted, we don't actually want the stream, so we can get rid of it now.
|
||||||
// it now.
|
|
||||||
if (videoMuted) {
|
if (videoMuted) {
|
||||||
previewStream.getTracks().forEach(t => t.stop());
|
stream?.getTracks().forEach(t => t.stop());
|
||||||
previewStream = undefined;
|
stream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [previewStream, devices[MediaDeviceKindEnum.AudioInput], devices[MediaDeviceKindEnum.VideoInput]];
|
return [stream, devices.audioinput, devices.videoinput];
|
||||||
}, [videoInputId, videoMuted], [null, [], []]);
|
}, [videoInputId, videoMuted], [null, [], []]);
|
||||||
|
|
||||||
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
||||||
|
@ -188,7 +190,7 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallBu
|
||||||
videoElement.play();
|
videoElement.play();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
videoStream?.getTracks().forEach(track => track.stop());
|
videoStream.getTracks().forEach(track => track.stop());
|
||||||
videoElement.srcObject = null;
|
videoElement.srcObject = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -358,7 +360,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
||||||
lobby = <Lobby
|
lobby = <Lobby
|
||||||
room={room}
|
room={room}
|
||||||
connect={connect}
|
connect={connect}
|
||||||
joinCallButtonTooltip={joinCallButtonTooltip}
|
joinCallButtonTooltip={joinCallButtonTooltip ?? undefined}
|
||||||
joinCallButtonDisabled={joinCallButtonDisabled}
|
joinCallButtonDisabled={joinCallButtonDisabled}
|
||||||
>
|
>
|
||||||
{ facePile }
|
{ facePile }
|
||||||
|
|
|
@ -816,7 +816,7 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare);
|
this.messaging!.on(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async performDisconnection(): Promise<void> {
|
protected async performDisconnection(): Promise<void> {
|
||||||
|
@ -832,7 +832,7 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||||
super.setDisconnected();
|
super.setDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -952,19 +952,24 @@ export class ElementCall extends Call {
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
};
|
};
|
||||||
|
|
||||||
private onScreenshare = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
private onScreenshareRequest = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (PlatformPeg.get().supportsDesktopCapturer()) {
|
if (PlatformPeg.get().supportsDesktopCapturer()) {
|
||||||
|
await this.messaging!.transport.reply(ev.detail, { pending: true });
|
||||||
|
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
|
|
||||||
await this.messaging!.transport.reply(ev.detail, {
|
if (source) {
|
||||||
failed: !source,
|
await this.messaging!.transport.send(ElementWidgetActions.ScreenshareStart, {
|
||||||
desktopCapturerSourceId: source,
|
desktopCapturerSourceId: source,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await this.messaging!.transport.send(ElementWidgetActions.ScreenshareStop, {});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.messaging!.transport.reply(ev.detail, {});
|
await this.messaging!.transport.reply(ev.detail, { pending: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,23 @@ export enum ElementWidgetActions {
|
||||||
MuteVideo = "io.element.mute_video",
|
MuteVideo = "io.element.mute_video",
|
||||||
UnmuteVideo = "io.element.unmute_video",
|
UnmuteVideo = "io.element.unmute_video",
|
||||||
StartLiveStream = "im.vector.start_live_stream",
|
StartLiveStream = "im.vector.start_live_stream",
|
||||||
|
|
||||||
|
// Element Call -> host requesting to start a screenshare
|
||||||
|
// (ie. expects a ScreenshareStart once the user has picked a source)
|
||||||
|
// replies with { pending } where pending is true if the host has asked
|
||||||
|
// the user to choose a window and false if not (ie. if the host isn't
|
||||||
|
// running within Electron)
|
||||||
|
ScreenshareRequest = "io.element.screenshare_request",
|
||||||
|
// host -> Element Call telling EC to start screen sharing with
|
||||||
|
// the given source
|
||||||
|
ScreenshareStart = "io.element.screenshare_start",
|
||||||
|
// host -> Element Call telling EC to stop screen sharing, or that
|
||||||
|
// the user cancelled when selecting a source after a ScreenshareRequest
|
||||||
|
ScreenshareStop = "io.element.screenshare_stop",
|
||||||
|
|
||||||
// Actions for switching layouts
|
// Actions for switching layouts
|
||||||
TileLayout = "io.element.tile_layout",
|
TileLayout = "io.element.tile_layout",
|
||||||
SpotlightLayout = "io.element.spotlight_layout",
|
SpotlightLayout = "io.element.spotlight_layout",
|
||||||
Screenshare = "io.element.screenshare",
|
|
||||||
|
|
||||||
OpenIntegrationManager = "integration_manager_open",
|
OpenIntegrationManager = "integration_manager_open",
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,12 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||||
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
|
this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`);
|
||||||
|
|
||||||
|
this.allowedCapabilities.add(
|
||||||
|
WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw,
|
||||||
|
);
|
||||||
|
this.allowedCapabilities.add(
|
||||||
|
WidgetEventCapability.forRoomEvent(EventDirection.Receive, "org.matrix.rageshake_request").raw,
|
||||||
|
);
|
||||||
this.allowedCapabilities.add(
|
this.allowedCapabilities.add(
|
||||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
|
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,10 @@ export enum VoiceBroadcastPlaybackEvent {
|
||||||
|
|
||||||
interface EventMap {
|
interface EventMap {
|
||||||
[VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void;
|
[VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void;
|
||||||
[VoiceBroadcastPlaybackEvent.StateChanged]: (state: VoiceBroadcastPlaybackState) => void;
|
[VoiceBroadcastPlaybackEvent.StateChanged]: (
|
||||||
|
state: VoiceBroadcastPlaybackState,
|
||||||
|
playback: VoiceBroadcastPlayback
|
||||||
|
) => void;
|
||||||
[VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void;
|
[VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,14 +220,20 @@ export class VoiceBroadcastPlayback
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public pause(): void {
|
||||||
if (!this.currentlyPlaying) return;
|
// stopped voice broadcasts cannot be paused
|
||||||
|
if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return;
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Paused);
|
this.setState(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
if (!this.currentlyPlaying) return;
|
||||||
this.currentlyPlaying.pause();
|
this.currentlyPlaying.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): void {
|
public resume(): void {
|
||||||
if (!this.currentlyPlaying) return;
|
if (!this.currentlyPlaying) {
|
||||||
|
// no playback to resume, start from the beginning
|
||||||
|
this.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Playing);
|
this.setState(VoiceBroadcastPlaybackState.Playing);
|
||||||
this.currentlyPlaying.play();
|
this.currentlyPlaying.play();
|
||||||
|
@ -260,7 +269,7 @@ export class VoiceBroadcastPlayback
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state);
|
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getInfoState(): VoiceBroadcastInfoState {
|
public getInfoState(): VoiceBroadcastInfoState {
|
||||||
|
|
|
@ -17,7 +17,8 @@ limitations under the License.
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||||
|
|
||||||
import { VoiceBroadcastPlayback } from "..";
|
import { VoiceBroadcastPlayback, VoiceBroadcastPlaybackEvent, VoiceBroadcastPlaybackState } from "..";
|
||||||
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybacksStoreEvent {
|
export enum VoiceBroadcastPlaybacksStoreEvent {
|
||||||
CurrentChanged = "current_changed",
|
CurrentChanged = "current_changed",
|
||||||
|
@ -28,10 +29,16 @@ interface EventMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This store provides access to the current and specific Voice Broadcast playbacks.
|
* This store manages VoiceBroadcastPlaybacks:
|
||||||
|
* - access the currently playing voice broadcast
|
||||||
|
* - ensures that only once broadcast is playing at a time
|
||||||
*/
|
*/
|
||||||
export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter<VoiceBroadcastPlaybacksStoreEvent, EventMap> {
|
export class VoiceBroadcastPlaybacksStore
|
||||||
|
extends TypedEventEmitter<VoiceBroadcastPlaybacksStoreEvent, EventMap>
|
||||||
|
implements IDestroyable {
|
||||||
private current: VoiceBroadcastPlayback | null;
|
private current: VoiceBroadcastPlayback | null;
|
||||||
|
|
||||||
|
/** Playbacks indexed by their info event id. */
|
||||||
private playbacks = new Map<string, VoiceBroadcastPlayback>();
|
private playbacks = new Map<string, VoiceBroadcastPlayback>();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
|
@ -42,7 +49,7 @@ export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter<VoiceBroadca
|
||||||
if (this.current === current) return;
|
if (this.current === current) return;
|
||||||
|
|
||||||
this.current = current;
|
this.current = current;
|
||||||
this.playbacks.set(current.infoEvent.getId(), current);
|
this.addPlayback(current);
|
||||||
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current);
|
this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,12 +61,51 @@ export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter<VoiceBroadca
|
||||||
const infoEventId = infoEvent.getId();
|
const infoEventId = infoEvent.getId();
|
||||||
|
|
||||||
if (!this.playbacks.has(infoEventId)) {
|
if (!this.playbacks.has(infoEventId)) {
|
||||||
this.playbacks.set(infoEventId, new VoiceBroadcastPlayback(infoEvent, client));
|
this.addPlayback(new VoiceBroadcastPlayback(infoEvent, client));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.playbacks.get(infoEventId);
|
return this.playbacks.get(infoEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addPlayback(playback: VoiceBroadcastPlayback): void {
|
||||||
|
const infoEventId = playback.infoEvent.getId();
|
||||||
|
|
||||||
|
if (this.playbacks.has(infoEventId)) return;
|
||||||
|
|
||||||
|
this.playbacks.set(infoEventId, playback);
|
||||||
|
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPlaybackStateChanged = (
|
||||||
|
state: VoiceBroadcastPlaybackState,
|
||||||
|
playback: VoiceBroadcastPlayback,
|
||||||
|
): void => {
|
||||||
|
if ([
|
||||||
|
VoiceBroadcastPlaybackState.Buffering,
|
||||||
|
VoiceBroadcastPlaybackState.Playing,
|
||||||
|
].includes(state)) {
|
||||||
|
this.pauseExcept(playback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private pauseExcept(playbackNotToPause: VoiceBroadcastPlayback): void {
|
||||||
|
for (const playback of this.playbacks.values()) {
|
||||||
|
if (playback !== playbackNotToPause) {
|
||||||
|
playback.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.removeAllListeners();
|
||||||
|
|
||||||
|
for (const playback of this.playbacks.values()) {
|
||||||
|
playback.off(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playbacks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly _instance = new VoiceBroadcastPlaybacksStore();
|
public static readonly _instance = new VoiceBroadcastPlaybacksStore();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -820,19 +820,25 @@ describe("ElementCall", () => {
|
||||||
await call.connect();
|
await call.connect();
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(
|
||||||
`action:${ElementWidgetActions.Screenshare}`,
|
`action:${ElementWidgetActions.ScreenshareRequest}`,
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||||
);
|
);
|
||||||
|
|
||||||
waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({}),
|
expect.objectContaining({}),
|
||||||
expect.objectContaining({ desktopCapturerSourceId: sourceId }),
|
expect.objectContaining({ pending: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(messaging!.transport.send).toHaveBeenCalledWith(
|
||||||
|
"io.element.screenshare_start", expect.objectContaining({ desktopCapturerSourceId: sourceId }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes failed if we couldn't get a source id", async () => {
|
it("sends ScreenshareStop if we couldn't get a source id", async () => {
|
||||||
jest.spyOn(Modal, "createDialog").mockReturnValue(
|
jest.spyOn(Modal, "createDialog").mockReturnValue(
|
||||||
{ finished: new Promise((r) => r([null])) } as IHandle<any[]>,
|
{ finished: new Promise((r) => r([null])) } as IHandle<any[]>,
|
||||||
);
|
);
|
||||||
|
@ -841,32 +847,38 @@ describe("ElementCall", () => {
|
||||||
await call.connect();
|
await call.connect();
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(
|
||||||
`action:${ElementWidgetActions.Screenshare}`,
|
`action:${ElementWidgetActions.ScreenshareRequest}`,
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||||
);
|
);
|
||||||
|
|
||||||
waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({}),
|
expect.objectContaining({}),
|
||||||
expect.objectContaining({ failed: true }),
|
expect.objectContaining({ pending: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(messaging!.transport.send).toHaveBeenCalledWith(
|
||||||
|
"io.element.screenshare_stop", expect.objectContaining({ }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes an empty object if we don't support desktop capturer", async () => {
|
it("replies with pending: false if we don't support desktop capturer", async () => {
|
||||||
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false);
|
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false);
|
||||||
|
|
||||||
await call.connect();
|
await call.connect();
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(
|
||||||
`action:${ElementWidgetActions.Screenshare}`,
|
`action:${ElementWidgetActions.ScreenshareRequest}`,
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||||
);
|
);
|
||||||
|
|
||||||
waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
expect(messaging!.transport.reply).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({}),
|
expect.objectContaining({}),
|
||||||
expect.objectContaining({}),
|
expect.objectContaining({ pending: false }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -66,6 +66,8 @@ describe("StopGapWidgetDriver", () => {
|
||||||
"m.always_on_screen",
|
"m.always_on_screen",
|
||||||
"town.robin.msc3846.turn_servers",
|
"town.robin.msc3846.turn_servers",
|
||||||
"org.matrix.msc2762.timeline:!1:example.org",
|
"org.matrix.msc2762.timeline:!1:example.org",
|
||||||
|
"org.matrix.msc2762.send.event:org.matrix.rageshake_request",
|
||||||
|
"org.matrix.msc2762.receive.event:org.matrix.rageshake_request",
|
||||||
"org.matrix.msc2762.receive.state_event:m.room.member",
|
"org.matrix.msc2762.receive.state_event:m.room.member",
|
||||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call",
|
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call",
|
||||||
"org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call",
|
"org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call",
|
||||||
|
|
|
@ -74,7 +74,25 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
|
|
||||||
const itShouldEmitAStateChangedEvent = (state: VoiceBroadcastPlaybackState) => {
|
const itShouldEmitAStateChangedEvent = (state: VoiceBroadcastPlaybackState) => {
|
||||||
it(`should emit a ${state} state changed event`, () => {
|
it(`should emit a ${state} state changed event`, () => {
|
||||||
expect(mocked(onStateChanged)).toHaveBeenCalledWith(state);
|
expect(mocked(onStateChanged)).toHaveBeenCalledWith(state, playback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPlayback = () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await playback.start();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const pausePlayback = () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playback.pause();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPlayback = () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playback.stop();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -180,14 +198,28 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(async () => {
|
startPlayback();
|
||||||
await playback.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be in buffering state", () => {
|
it("should be in buffering state", () => {
|
||||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Buffering);
|
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Buffering);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and calling stop", () => {
|
||||||
|
stopPlayback();
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
|
describe("and calling pause", () => {
|
||||||
|
pausePlayback();
|
||||||
|
// stopped voice broadcasts cannot be paused
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and calling pause", () => {
|
||||||
|
pausePlayback();
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
});
|
||||||
|
|
||||||
describe("and receiving the first chunk", () => {
|
describe("and receiving the first chunk", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// TODO Michael W: Use RelationsHelper
|
// TODO Michael W: Use RelationsHelper
|
||||||
|
@ -212,9 +244,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(async () => {
|
startPlayback();
|
||||||
await playback.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should play the last chunk", () => {
|
it("should play the last chunk", () => {
|
||||||
// assert that the last chunk is played first
|
// assert that the last chunk is played first
|
||||||
|
@ -258,10 +288,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(async () => {
|
startPlayback();
|
||||||
await playback.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -278,9 +305,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(async () => {
|
startPlayback();
|
||||||
await playback.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
|
||||||
|
@ -303,13 +328,15 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling pause", () => {
|
describe("and calling pause", () => {
|
||||||
beforeEach(() => {
|
pausePlayback();
|
||||||
playback.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and calling stop", () => {
|
||||||
|
stopPlayback();
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling toggle for the first time", () => {
|
describe("and calling toggle for the first time", () => {
|
||||||
|
@ -337,9 +364,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling stop", () => {
|
describe("and calling stop", () => {
|
||||||
beforeEach(() => {
|
stopPlayback();
|
||||||
playback.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
|
|
|
@ -22,24 +22,24 @@ import {
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VoiceBroadcastInfoEventType,
|
VoiceBroadcastInfoState,
|
||||||
VoiceBroadcastPlayback,
|
VoiceBroadcastPlayback,
|
||||||
|
VoiceBroadcastPlaybackEvent,
|
||||||
VoiceBroadcastPlaybacksStore,
|
VoiceBroadcastPlaybacksStore,
|
||||||
VoiceBroadcastPlaybacksStoreEvent,
|
VoiceBroadcastPlaybacksStoreEvent,
|
||||||
|
VoiceBroadcastPlaybackState,
|
||||||
} from "../../../src/voice-broadcast";
|
} from "../../../src/voice-broadcast";
|
||||||
import { mkEvent, mkStubRoom, stubClient } from "../../test-utils";
|
import { mkStubRoom, stubClient } from "../../test-utils";
|
||||||
|
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastPlayback", () => ({
|
|
||||||
...jest.requireActual("../../../src/voice-broadcast/models/VoiceBroadcastPlayback") as object,
|
|
||||||
VoiceBroadcastPlayback: jest.fn().mockImplementation((infoEvent: MatrixEvent) => ({ infoEvent })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("VoiceBroadcastPlaybacksStore", () => {
|
describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let infoEvent: MatrixEvent;
|
let infoEvent1: MatrixEvent;
|
||||||
let playback: VoiceBroadcastPlayback;
|
let infoEvent2: MatrixEvent;
|
||||||
|
let playback1: VoiceBroadcastPlayback;
|
||||||
|
let playback2: VoiceBroadcastPlayback;
|
||||||
let playbacks: VoiceBroadcastPlaybacksStore;
|
let playbacks: VoiceBroadcastPlaybacksStore;
|
||||||
let onCurrentChanged: (playback: VoiceBroadcastPlayback) => void;
|
let onCurrentChanged: (playback: VoiceBroadcastPlayback) => void;
|
||||||
|
|
||||||
|
@ -51,17 +51,26 @@ describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
infoEvent = mkEvent({
|
|
||||||
event: true,
|
infoEvent1 = mkVoiceBroadcastInfoStateEvent(
|
||||||
type: VoiceBroadcastInfoEventType,
|
roomId,
|
||||||
user: client.getUserId(),
|
VoiceBroadcastInfoState.Started,
|
||||||
room: roomId,
|
client.getUserId(),
|
||||||
content: {},
|
client.getDeviceId(),
|
||||||
});
|
);
|
||||||
playback = {
|
infoEvent2 = mkVoiceBroadcastInfoStateEvent(
|
||||||
infoEvent,
|
roomId,
|
||||||
} as unknown as VoiceBroadcastPlayback;
|
VoiceBroadcastInfoState.Started,
|
||||||
|
client.getUserId(),
|
||||||
|
client.getDeviceId(),
|
||||||
|
);
|
||||||
|
playback1 = new VoiceBroadcastPlayback(infoEvent1, client);
|
||||||
|
jest.spyOn(playback1, "off");
|
||||||
|
playback2 = new VoiceBroadcastPlayback(infoEvent2, client);
|
||||||
|
jest.spyOn(playback2, "off");
|
||||||
|
|
||||||
playbacks = new VoiceBroadcastPlaybacksStore();
|
playbacks = new VoiceBroadcastPlaybacksStore();
|
||||||
|
jest.spyOn(playbacks, "removeAllListeners");
|
||||||
onCurrentChanged = jest.fn();
|
onCurrentChanged = jest.fn();
|
||||||
playbacks.on(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged);
|
playbacks.on(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged);
|
||||||
});
|
});
|
||||||
|
@ -72,31 +81,69 @@ describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
|
|
||||||
describe("when setting a current Voice Broadcast playback", () => {
|
describe("when setting a current Voice Broadcast playback", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
playbacks.setCurrent(playback);
|
playbacks.setCurrent(playback1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return it as current", () => {
|
it("should return it as current", () => {
|
||||||
expect(playbacks.getCurrent()).toBe(playback);
|
expect(playbacks.getCurrent()).toBe(playback1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return it by id", () => {
|
it("should return it by id", () => {
|
||||||
expect(playbacks.getByInfoEvent(infoEvent, client)).toBe(playback);
|
expect(playbacks.getByInfoEvent(infoEvent1, client)).toBe(playback1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit a CurrentChanged event", () => {
|
it("should emit a CurrentChanged event", () => {
|
||||||
expect(onCurrentChanged).toHaveBeenCalledWith(playback);
|
expect(onCurrentChanged).toHaveBeenCalledWith(playback1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and setting the same again", () => {
|
describe("and setting the same again", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocked(onCurrentChanged).mockClear();
|
mocked(onCurrentChanged).mockClear();
|
||||||
playbacks.setCurrent(playback);
|
playbacks.setCurrent(playback1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not emit a CurrentChanged event", () => {
|
it("should not emit a CurrentChanged event", () => {
|
||||||
expect(onCurrentChanged).not.toHaveBeenCalled();
|
expect(onCurrentChanged).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and setting another playback and start both", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playbacks.setCurrent(playback2);
|
||||||
|
playback1.start();
|
||||||
|
playback2.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set playback1 to paused", () => {
|
||||||
|
expect(playback1.getState()).toBe(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set playback2 to buffering", () => {
|
||||||
|
// buffering because there are no chunks, yet
|
||||||
|
expect(playback2.getState()).toBe(VoiceBroadcastPlaybackState.Buffering);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and calling destroy", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playbacks.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove all listeners", () => {
|
||||||
|
expect(playbacks.removeAllListeners).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deregister the listeners on the playbacks", () => {
|
||||||
|
expect(playback1.off).toHaveBeenCalledWith(
|
||||||
|
VoiceBroadcastPlaybackEvent.StateChanged,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(playback2.off).toHaveBeenCalledWith(
|
||||||
|
VoiceBroadcastPlaybackEvent.StateChanged,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getByInfoEventId", () => {
|
describe("getByInfoEventId", () => {
|
||||||
|
@ -104,24 +151,22 @@ describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
|
|
||||||
describe("when retrieving a known playback", () => {
|
describe("when retrieving a known playback", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
playbacks.setCurrent(playback);
|
playbacks.setCurrent(playback1);
|
||||||
returnedPlayback = playbacks.getByInfoEvent(infoEvent, client);
|
returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the playback", () => {
|
it("should return the playback", () => {
|
||||||
expect(returnedPlayback).toBe(playback);
|
expect(returnedPlayback).toBe(playback1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when retrieving an unknown playback", () => {
|
describe("when retrieving an unknown playback", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
returnedPlayback = playbacks.getByInfoEvent(infoEvent, client);
|
returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the playback", () => {
|
it("should return the playback", () => {
|
||||||
expect(returnedPlayback).toEqual({
|
expect(returnedPlayback.infoEvent).toBe(infoEvent1);
|
||||||
infoEvent,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue