Merge branch 'develop' into feat/add-message-edition-wysiwyg-composer

This commit is contained in:
Florian Duros 2022-10-25 10:32:29 +02:00 committed by GitHub
commit 787acbcc31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 273 additions and 105 deletions

View file

@ -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) {

View file

@ -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 }

View file

@ -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 });
} }
}; };
} }

View file

@ -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",

View file

@ -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,
); );

View file

@ -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 {

View file

@ -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();
/** /**

View file

@ -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 }),
); );
}); });
}); });

View file

@ -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",

View file

@ -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);

View file

@ -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,
});
}); });
}); });
}); });