Merge branch 'develop' into travis/remove-skinning

This commit is contained in:
Travis Ralston 2022-04-05 10:50:37 -06:00
commit 4057833036
74 changed files with 1412 additions and 1717 deletions

View file

@ -0,0 +1,164 @@
/*
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 { EventEmitter } from "events";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "./ActiveWidgetStore";
import {
VIDEO_CHANNEL,
VIDEO_CHANNEL_MEMBER,
IVideoChannelMemberContent,
getVideoChannel,
} from "../utils/VideoChannelUtils";
import WidgetUtils from "../utils/WidgetUtils";
export enum VideoChannelEvent {
Connect = "connect",
Disconnect = "disconnect",
Participants = "participants",
}
export interface IJitsiParticipant {
avatarURL: string;
displayName: string;
formattedDisplayName: string;
participantId: string;
}
/*
* Holds information about the currently active video channel.
*/
export default class VideoChannelStore extends EventEmitter {
private static _instance: VideoChannelStore;
public static get instance(): VideoChannelStore {
if (!VideoChannelStore._instance) {
VideoChannelStore._instance = new VideoChannelStore();
}
return VideoChannelStore._instance;
}
private readonly cli = MatrixClientPeg.get();
private activeChannel: ClientWidgetApi;
private _roomId: string;
private _participants: IJitsiParticipant[];
public get roomId(): string {
return this._roomId;
}
public get participants(): IJitsiParticipant[] {
return this._participants;
}
public start = () => {
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate);
};
public stop = () => {
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate);
};
private setConnected = async (roomId: string) => {
const jitsi = getVideoChannel(roomId);
if (!jitsi) throw new Error(`No video channel in room ${roomId}`);
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi));
if (!messaging) throw new Error(`Failed to bind video channel in room ${roomId}`);
this.activeChannel = messaging;
this._roomId = roomId;
this._participants = [];
this.activeChannel.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.activeChannel.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
this.emit(VideoChannelEvent.Connect);
// Tell others that we're connected, by adding our device to room state
await this.updateDevices(devices => Array.from(new Set(devices).add(this.cli.getDeviceId())));
};
private setDisconnected = async () => {
this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
this.activeChannel = null;
this._participants = null;
try {
// Tell others that we're disconnected, by removing our device from room state
await this.updateDevices(devices => {
const devicesSet = new Set(devices);
devicesSet.delete(this.cli.getDeviceId());
return Array.from(devicesSet);
});
} finally {
// Save this for last, since updateDevices needs the room ID
this._roomId = null;
this.emit(VideoChannelEvent.Disconnect);
}
};
private ack = (ev: CustomEvent<IWidgetApiRequest>) => {
// Even if we don't have a reply to a given widget action, we still need
// to give the widget API something to acknowledge receipt
this.activeChannel.transport.reply(ev.detail, {});
};
private updateDevices = async (fn: (devices: string[]) => string[]) => {
if (!this.roomId) {
logger.error("Tried to update devices while disconnected");
return;
}
const room = this.cli.getRoom(this.roomId);
const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, this.cli.getUserId());
const devices = devicesState?.getContent<IVideoChannelMemberContent>()?.devices ?? [];
await this.cli.sendStateEvent(
this.roomId, VIDEO_CHANNEL_MEMBER, { devices: fn(devices) }, this.cli.getUserId(),
);
};
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
this.ack(ev);
await this.setDisconnected();
};
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
this._participants = ev.detail.data.participants as IJitsiParticipant[];
this.emit(VideoChannelEvent.Participants, ev.detail.data.participants);
this.ack(ev);
};
private onActiveWidgetUpdate = async () => {
if (this.activeChannel) {
// We got disconnected from the previous video channel, so clean up
await this.setDisconnected();
}
// If the new active widget is a video channel, that means we joined
if (ActiveWidgetStore.instance.getPersistentWidgetId() === VIDEO_CHANNEL) {
await this.setConnected(ActiveWidgetStore.instance.getPersistentRoomId());
}
};
}

View file

@ -1,267 +0,0 @@
/*
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 { EventEmitter } from "events";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
import {
VOICE_CHANNEL_MEMBER,
IVoiceChannelMemberContent,
getVoiceChannel,
} from "../utils/VoiceChannelUtils";
import { timeout } from "../utils/promise";
import WidgetUtils from "../utils/WidgetUtils";
export enum VoiceChannelEvent {
Connect = "connect",
Disconnect = "disconnect",
Participants = "participants",
MuteAudio = "mute_audio",
UnmuteAudio = "unmute_audio",
MuteVideo = "mute_video",
UnmuteVideo = "unmute_video",
}
export interface IJitsiParticipant {
avatarURL: string;
displayName: string;
formattedDisplayName: string;
participantId: string;
}
/*
* Holds information about the currently active voice channel.
*/
export default class VoiceChannelStore extends EventEmitter {
private static _instance: VoiceChannelStore;
private static readonly TIMEOUT = 8000;
public static get instance(): VoiceChannelStore {
if (!VoiceChannelStore._instance) {
VoiceChannelStore._instance = new VoiceChannelStore();
}
return VoiceChannelStore._instance;
}
private readonly cli = MatrixClientPeg.get();
private activeChannel: ClientWidgetApi;
private _roomId: string;
private _participants: IJitsiParticipant[];
private _audioMuted: boolean;
private _videoMuted: boolean;
public get roomId(): string {
return this._roomId;
}
public get participants(): IJitsiParticipant[] {
return this._participants;
}
public get audioMuted(): boolean {
return this._audioMuted;
}
public get videoMuted(): boolean {
return this._videoMuted;
}
public connect = async (roomId: string) => {
if (this.activeChannel) await this.disconnect();
const jitsi = getVoiceChannel(roomId);
if (!jitsi) throw new Error(`No voice channel in room ${roomId}`);
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi));
if (!messaging) throw new Error(`Failed to bind voice channel in room ${roomId}`);
this.activeChannel = messaging;
this._roomId = roomId;
// Participant data and mute state will come down the event pipeline very quickly,
// so prepare in advance
messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
// Actually perform the join
const waitForJoin = this.waitForAction(ElementWidgetActions.JoinCall);
messaging.transport.send(ElementWidgetActions.JoinCall, {});
try {
await waitForJoin;
} catch (e) {
// If it timed out, clean up our advance preparations
this.activeChannel = null;
this._roomId = null;
messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
throw e;
}
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.emit(VoiceChannelEvent.Connect);
// Tell others that we're connected, by adding our device to room state
await this.updateDevices(devices => Array.from(new Set(devices).add(this.cli.getDeviceId())));
};
public disconnect = async () => {
this.assertConnected();
const waitForHangup = this.waitForAction(ElementWidgetActions.HangupCall);
this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {});
await waitForHangup;
// onHangup cleans up for us
};
public muteAudio = async () => {
this.assertConnected();
const waitForMute = this.waitForAction(ElementWidgetActions.MuteAudio);
this.activeChannel.transport.send(ElementWidgetActions.MuteAudio, {});
await waitForMute;
};
public unmuteAudio = async () => {
this.assertConnected();
const waitForUnmute = this.waitForAction(ElementWidgetActions.UnmuteAudio);
this.activeChannel.transport.send(ElementWidgetActions.UnmuteAudio, {});
await waitForUnmute;
};
public muteVideo = async () => {
this.assertConnected();
const waitForMute = this.waitForAction(ElementWidgetActions.MuteVideo);
this.activeChannel.transport.send(ElementWidgetActions.MuteVideo, {});
await waitForMute;
};
public unmuteVideo = async () => {
this.assertConnected();
const waitForUnmute = this.waitForAction(ElementWidgetActions.UnmuteVideo);
this.activeChannel.transport.send(ElementWidgetActions.UnmuteVideo, {});
await waitForUnmute;
};
private assertConnected = () => {
if (!this.activeChannel) throw new Error("Not connected to any voice channel");
};
private waitForAction = async (action: ElementWidgetActions) => {
const wait = new Promise<void>(resolve =>
this.activeChannel.once(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
this.ack(ev);
resolve();
}),
);
if (await timeout(wait, false, VoiceChannelStore.TIMEOUT) === false) {
throw new Error("Communication with voice channel timed out");
}
};
private ack = (ev: CustomEvent<IWidgetApiRequest>) => {
this.activeChannel.transport.reply(ev.detail, {});
};
private updateDevices = async (fn: (devices: string[]) => string[]) => {
if (!this.roomId) {
logger.error("Tried to update devices while disconnected");
return;
}
const devices = this.cli.getRoom(this.roomId)
.currentState.getStateEvents(VOICE_CHANNEL_MEMBER, this.cli.getUserId())
?.getContent<IVoiceChannelMemberContent>()?.devices ?? [];
await this.cli.sendStateEvent(
this.roomId, VOICE_CHANNEL_MEMBER, { devices: fn(devices) }, this.cli.getUserId(),
);
};
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
this.ack(ev);
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
this.activeChannel.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
this.activeChannel.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
this.activeChannel = null;
this._participants = null;
this._audioMuted = null;
this._videoMuted = null;
try {
// Tell others that we're disconnected, by removing our device from room state
await this.updateDevices(devices => {
const devicesSet = new Set(devices);
devicesSet.delete(this.cli.getDeviceId());
return Array.from(devicesSet);
});
} finally {
// Save this for last, since updateDevices needs the room ID
this._roomId = null;
this.emit(VoiceChannelEvent.Disconnect);
}
};
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
this._participants = ev.detail.data.participants as IJitsiParticipant[];
this.emit(VoiceChannelEvent.Participants, ev.detail.data.participants);
this.ack(ev);
};
private onMuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
this._audioMuted = true;
this.emit(VoiceChannelEvent.MuteAudio);
this.ack(ev);
};
private onUnmuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
this._audioMuted = false;
this.emit(VoiceChannelEvent.UnmuteAudio);
this.ack(ev);
};
private onMuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
this._videoMuted = true;
this.emit(VoiceChannelEvent.MuteVideo);
this.ack(ev);
};
private onUnmuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
this._videoMuted = false;
this.emit(VoiceChannelEvent.UnmuteVideo);
this.ack(ev);
};
}

View file

@ -1116,7 +1116,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
case Action.ViewRoom: {
// Don't auto-switch rooms when reacting to a context-switch or for new rooms being created
// as this is not helpful and can create loops of rooms/space switching
if (payload.context_switch || payload.justCreatedOpts) break;
const isSpace = payload.justCreatedOpts?.roomType === RoomType.Space;
if (payload.context_switch || (payload.justCreatedOpts && !isSpace)) break;
let roomId = payload.room_id;
if (payload.room_alias && !roomId) {