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

Commit d7b4740

Browse files
authored
Prevent multiple Jitsi calls started at the same time (#10183)
1 parent b9ff655 commit d7b4740

File tree

2 files changed

+150
-1
lines changed

2 files changed

+150
-1
lines changed

src/components/structures/RoomView.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ import { RoomSearchView } from "./RoomSearchView";
114114
import eventSearch from "../../Searching";
115115
import VoipUserMapper from "../../VoipUserMapper";
116116
import { isCallEvent } from "./LegacyCallEventGrouper";
117+
import { WidgetType } from "../../widgets/WidgetType";
118+
import WidgetUtils from "../../utils/WidgetUtils";
117119

118120
const DEBUG = false;
121+
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
119122
let debuglog = function (msg: string): void {};
120123

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

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

510+
/**
511+
* Removes the Jitsi widget from the current user if
512+
* - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN}
513+
* - The last (server timestamp) of these widgets is from the currrent user
514+
* This solves the issue if some people decide to start a conference and click the call button at the same time.
515+
*/
516+
private doMaybeRemoveOwnJitsiWidget(): void {
517+
if (!this.state.roomId || !this.state.room || !this.context.client) return;
518+
519+
const apps = this.context.widgetStore.getApps(this.state.roomId);
520+
const jitsiApps = apps.filter((app) => app.eventId && WidgetType.JITSI.matches(app.type));
521+
522+
// less than two Jitsi widgets → nothing to do
523+
if (jitsiApps.length < 2) return;
524+
525+
const currentUserId = this.context.client.getSafeUserId();
526+
const createdByCurrentUser = jitsiApps.find((apps) => apps.creatorUserId === currentUserId);
527+
528+
// no Jitsi widget from current user → nothing to do
529+
if (!createdByCurrentUser) return;
530+
531+
const createdByCurrentUserEvent = this.state.room.findEventById(createdByCurrentUser.eventId!);
532+
533+
// widget event not found → nothing can be done
534+
if (!createdByCurrentUserEvent) return;
535+
536+
const createdByCurrentUserTs = createdByCurrentUserEvent.getTs();
537+
538+
// widget timestamp is empty → nothing can be done
539+
if (!createdByCurrentUserTs) return;
540+
541+
const lastCreatedByOtherTs = jitsiApps.reduce((maxByNow: number, app) => {
542+
if (app.eventId === createdByCurrentUser.eventId) return maxByNow;
543+
544+
const appCreateTs = this.state.room!.findEventById(app.eventId!)?.getTs() || 0;
545+
return Math.max(maxByNow, appCreateTs);
546+
}, 0);
547+
548+
// last widget timestamp from other is empty → nothing can be done
549+
if (!lastCreatedByOtherTs) return;
550+
551+
if (
552+
createdByCurrentUserTs > lastCreatedByOtherTs &&
553+
createdByCurrentUserTs - lastCreatedByOtherTs < PREVENT_MULTIPLE_JITSI_WITHIN
554+
) {
555+
// more than one Jitsi widget with the last one from the current user → remove it
556+
WidgetUtils.setRoomWidget(this.state.roomId, createdByCurrentUser.id);
557+
}
558+
}
559+
506560
private checkWidgets = (room: Room): void => {
507561
this.setState({
508562
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),

test/components/structures/RoomView-test.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { mocked, MockedObject } from "jest-mock";
2121
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
2222
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
2323
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
24-
import { EventType } from "matrix-js-sdk/src/matrix";
24+
import { EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
2525
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
2626
import { fireEvent, render } from "@testing-library/react";
2727

@@ -31,6 +31,9 @@ import {
3131
unmockPlatformPeg,
3232
wrapInMatrixClientContext,
3333
flushPromises,
34+
mkEvent,
35+
setupAsyncStoreWithClient,
36+
filterConsole,
3437
} from "../../test-utils";
3538
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
3639
import { Action } from "../../../src/dispatcher/actions";
@@ -49,6 +52,9 @@ import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
4952
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
5053
import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext";
5154
import VoipUserMapper from "../../../src/VoipUserMapper";
55+
import WidgetUtils from "../../../src/utils/WidgetUtils";
56+
import { WidgetType } from "../../../src/widgets/WidgetType";
57+
import WidgetStore from "../../../src/stores/WidgetStore";
5258

5359
const RoomView = wrapInMatrixClientContext(_RoomView);
5460

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

68+
// mute some noise
69+
filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability");
70+
6271
beforeEach(async () => {
6372
mockPlatformPeg({ reload: () => {} });
6473
stubClient();
@@ -359,4 +368,90 @@ describe("RoomView", () => {
359368
});
360369
});
361370
});
371+
372+
describe("when there is a RoomView", () => {
373+
const widget1Id = "widget1";
374+
const widget2Id = "widget2";
375+
const otherUserId = "@other:example.com";
376+
377+
const addJitsiWidget = async (id: string, user: string, ts?: number): Promise<void> => {
378+
const widgetEvent = mkEvent({
379+
event: true,
380+
room: room.roomId,
381+
user,
382+
type: "im.vector.modular.widgets",
383+
content: {
384+
id,
385+
name: "Jitsi",
386+
type: WidgetType.JITSI.preferred,
387+
url: "https://example.com",
388+
},
389+
skey: id,
390+
ts,
391+
});
392+
room.addLiveEvents([widgetEvent]);
393+
room.currentState.setStateEvents([widgetEvent]);
394+
cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null);
395+
await flushPromises();
396+
};
397+
398+
beforeEach(async () => {
399+
jest.spyOn(WidgetUtils, "setRoomWidget");
400+
const widgetStore = WidgetStore.instance;
401+
await setupAsyncStoreWithClient(widgetStore, cli);
402+
getRoomViewInstance();
403+
});
404+
405+
const itShouldNotRemoveTheLastWidget = (): void => {
406+
it("should not remove the last widget", (): void => {
407+
expect(WidgetUtils.setRoomWidget).not.toHaveBeenCalledWith(room.roomId, widget2Id);
408+
});
409+
};
410+
411+
describe("and there is a Jitsi widget from another user", () => {
412+
beforeEach(async () => {
413+
await addJitsiWidget(widget1Id, otherUserId, 10_000);
414+
});
415+
416+
describe("and the current user adds a Jitsi widget after 10s", () => {
417+
beforeEach(async () => {
418+
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 20_000);
419+
});
420+
421+
it("the last Jitsi widget should be removed", () => {
422+
expect(WidgetUtils.setRoomWidget).toHaveBeenCalledWith(room.roomId, widget2Id);
423+
});
424+
});
425+
426+
describe("and the current user adds a Jitsi widget after two minutes", () => {
427+
beforeEach(async () => {
428+
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 130_000);
429+
});
430+
431+
itShouldNotRemoveTheLastWidget();
432+
});
433+
434+
describe("and the current user adds a Jitsi widget without timestamp", () => {
435+
beforeEach(async () => {
436+
await addJitsiWidget(widget2Id, cli.getSafeUserId());
437+
});
438+
439+
itShouldNotRemoveTheLastWidget();
440+
});
441+
});
442+
443+
describe("and there is a Jitsi widget from another user without timestamp", () => {
444+
beforeEach(async () => {
445+
await addJitsiWidget(widget1Id, otherUserId);
446+
});
447+
448+
describe("and the current user adds a Jitsi widget", () => {
449+
beforeEach(async () => {
450+
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 10_000);
451+
});
452+
453+
itShouldNotRemoveTheLastWidget();
454+
});
455+
});
456+
});
362457
});

0 commit comments

Comments
 (0)