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

Commit

Permalink
Prevent multiple Jitsi calls started at the same time (#10183)
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 authored Feb 22, 2023
1 parent b9ff655 commit d7b4740
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 1 deletion.
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;
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();
});
});
});
});

0 comments on commit d7b4740

Please sign in to comment.