Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Prevent multiple Jitsi calls started at the same time #10183

Merged
merged 3 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ import { RoomSearchView } from "./RoomSearchView";
import eventSearch from "../../Searching";
import VoipUserMapper from "../../VoipUserMapper";
import { isCallEvent } from "./LegacyCallEventGrouper";
import { WidgetType } from "../../widgets/WidgetType";
import WidgetUtils from "../../utils/WidgetUtils";

const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
let debuglog = function (msg: string): void {};

const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe");
Expand Down Expand Up @@ -483,6 +486,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onWidgetStoreUpdate = (): void => {
if (!this.state.room) return;
this.checkWidgets(this.state.room);
this.doMaybeRemoveOwnJitsiWidget();
};

private onWidgetEchoStoreUpdate = (): void => {
Expand All @@ -503,6 +507,56 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.checkWidgets(this.state.room);
};

/**
* Removes the Jitsi widget from the current user if
* - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN}
* - The last (server timestamp) of these widgets is from the currrent user
* This solves the issue if some people decide to start a conference and click the call button at the same time.
*/
private doMaybeRemoveOwnJitsiWidget(): void {
if (!this.state.roomId || !this.state.room || !this.context.client) return;

const apps = this.context.widgetStore.getApps(this.state.roomId);
const jitsiApps = apps.filter((app) => app.eventId && WidgetType.JITSI.matches(app.type));

// less than two Jitsi widgets → nothing to do
if (jitsiApps.length < 2) return;

const currentUserId = this.context.client.getSafeUserId();
const createdByCurrentUser = jitsiApps.find((apps) => apps.creatorUserId === currentUserId);

// no Jitsi widget from current user → nothing to do
if (!createdByCurrentUser) return;

const createdByCurrentUserEvent = this.state.room.findEventById(createdByCurrentUser.eventId!);

// widget event not found → nothing can be done
if (!createdByCurrentUserEvent) return;

const createdByCurrentUserTs = createdByCurrentUserEvent.getTs();

// widget timestamp is empty → nothing can be done
if (!createdByCurrentUserTs) return;

const lastCreatedByOtherTs = jitsiApps.reduce((maxByNow: number, app) => {
if (app.eventId === createdByCurrentUser.eventId) return maxByNow;

const appCreateTs = this.state.room!.findEventById(app.eventId!)?.getTs() || 0;
return Math.max(maxByNow, appCreateTs);
}, 0);

// last widget timestamp from other is empty → nothing can be done
if (!lastCreatedByOtherTs) return;

if (
createdByCurrentUserTs > lastCreatedByOtherTs &&
createdByCurrentUserTs - lastCreatedByOtherTs < PREVENT_MULTIPLE_JITSI_WITHIN
) {
// more than one Jitsi widget with the last one from the current user → remove it
WidgetUtils.setRoomWidget(this.state.roomId, createdByCurrentUser.id);
}
}

private checkWidgets = (room: Room): void => {
this.setState({
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
Expand Down
97 changes: 96 additions & 1 deletion test/components/structures/RoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { mocked, MockedObject } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/matrix";
import { EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
import { fireEvent, render } from "@testing-library/react";

Expand All @@ -31,6 +31,9 @@ import {
unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises,
mkEvent,
setupAsyncStoreWithClient,
filterConsole,
} from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { Action } from "../../../src/dispatcher/actions";
Expand All @@ -49,6 +52,9 @@ import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext";
import VoipUserMapper from "../../../src/VoipUserMapper";
import WidgetUtils from "../../../src/utils/WidgetUtils";
import { WidgetType } from "../../../src/widgets/WidgetType";
import WidgetStore from "../../../src/stores/WidgetStore";

const RoomView = wrapInMatrixClientContext(_RoomView);

Expand All @@ -59,6 +65,9 @@ describe("RoomView", () => {
let roomCount = 0;
let stores: SdkContextClass;

// mute some noise
filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability");

beforeEach(async () => {
mockPlatformPeg({ reload: () => {} });
stubClient();
Expand Down Expand Up @@ -359,4 +368,90 @@ describe("RoomView", () => {
});
});
});

describe("when there is a RoomView", () => {
const widget1Id = "widget1";
const widget2Id = "widget2";
const otherUserId = "@other:example.com";

const addJitsiWidget = async (id: string, user: string, ts?: number): Promise<void> => {
const widgetEvent = mkEvent({
event: true,
room: room.roomId,
user,
type: "im.vector.modular.widgets",
content: {
id,
name: "Jitsi",
type: WidgetType.JITSI.preferred,
url: "https://example.com",
},
skey: id,
ts,
});
room.addLiveEvents([widgetEvent]);
room.currentState.setStateEvents([widgetEvent]);
cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null);
await flushPromises();
};

beforeEach(async () => {
jest.spyOn(WidgetUtils, "setRoomWidget");
const widgetStore = WidgetStore.instance;
await setupAsyncStoreWithClient(widgetStore, cli);
getRoomViewInstance();
});

const itShouldNotRemoveTheLastWidget = (): void => {
it("should not remove the last widget", (): void => {
expect(WidgetUtils.setRoomWidget).not.toHaveBeenCalledWith(room.roomId, widget2Id);
});
};

describe("and there is a Jitsi widget from another user", () => {
beforeEach(async () => {
await addJitsiWidget(widget1Id, otherUserId, 10_000);
});

describe("and the current user adds a Jitsi widget after 10s", () => {
beforeEach(async () => {
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 20_000);
});

it("the last Jitsi widget should be removed", () => {
expect(WidgetUtils.setRoomWidget).toHaveBeenCalledWith(room.roomId, widget2Id);
});
});

describe("and the current user adds a Jitsi widget after two minutes", () => {
beforeEach(async () => {
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 130_000);
});

itShouldNotRemoveTheLastWidget();
});

describe("and the current user adds a Jitsi widget without timestamp", () => {
beforeEach(async () => {
await addJitsiWidget(widget2Id, cli.getSafeUserId());
});

itShouldNotRemoveTheLastWidget();
});
});

describe("and there is a Jitsi widget from another user without timestamp", () => {
beforeEach(async () => {
await addJitsiWidget(widget1Id, otherUserId);
});

describe("and the current user adds a Jitsi widget", () => {
beforeEach(async () => {
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 10_000);
});

itShouldNotRemoveTheLastWidget();
});
});
});
});