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

Commit 4e8b731

Browse files
authored
Merge branch 'develop' into feat/reply-support-wysiwyg-composer
2 parents 4ba3f99 + 54008cf commit 4e8b731

File tree

7 files changed

+160
-20
lines changed

7 files changed

+160
-20
lines changed

src/components/views/messages/CallEvent.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
2020
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
2121
import { Call, ConnectionState } from "../../../models/Call";
2222
import { _t } from "../../../languageHandler";
23-
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
23+
import {
24+
useCall,
25+
useConnectionState,
26+
useJoinCallButtonDisabled,
27+
useJoinCallButtonTooltip,
28+
useParticipants,
29+
} from "../../../hooks/useCall";
2430
import defaultDispatcher from "../../../dispatcher/dispatcher";
2531
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
2632
import { Action } from "../../../dispatcher/actions";
@@ -106,7 +112,8 @@ interface ActiveLoadedCallEventProps {
106112
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
107113
const connectionState = useConnectionState(call);
108114
const participants = useParticipants(call);
109-
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
115+
const joinCallButtonTooltip = useJoinCallButtonTooltip(call);
116+
const joinCallButtonDisabled = useJoinCallButtonDisabled(call);
110117

111118
const connect = useCallback((ev: ButtonEvent) => {
112119
ev.preventDefault();
@@ -138,8 +145,8 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
138145
participants={participants}
139146
buttonText={buttonText}
140147
buttonKind={buttonKind}
141-
buttonDisabled={Boolean(joinCallButtonDisabledTooltip)}
142-
buttonTooltip={joinCallButtonDisabledTooltip}
148+
buttonDisabled={joinCallButtonDisabled}
149+
buttonTooltip={joinCallButtonTooltip}
143150
onButtonClick={onButtonClick}
144151
/>;
145152
});

src/components/views/voip/CallView.tsx

+16-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import { defer, IDeferred } from "matrix-js-sdk/src/utils";
2222
import type { Room } from "matrix-js-sdk/src/models/room";
2323
import type { ConnectionState } from "../../../models/Call";
2424
import { Call, CallEvent, ElementCall, isConnected } from "../../../models/Call";
25-
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
25+
import {
26+
useCall,
27+
useConnectionState,
28+
useJoinCallButtonDisabled,
29+
useJoinCallButtonTooltip,
30+
useParticipants,
31+
} from "../../../hooks/useCall";
2632
import MatrixClientContext from "../../../contexts/MatrixClientContext";
2733
import AppTile from "../elements/AppTile";
2834
import { _t } from "../../../languageHandler";
@@ -110,11 +116,12 @@ const MAX_FACES = 8;
110116
interface LobbyProps {
111117
room: Room;
112118
connect: () => Promise<void>;
113-
joinCallButtonDisabledTooltip?: string;
119+
joinCallButtonTooltip?: string;
120+
joinCallButtonDisabled?: boolean;
114121
children?: ReactNode;
115122
}
116123

117-
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, connect, children }) => {
124+
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallButtonTooltip, connect, children }) => {
118125
const [connecting, setConnecting] = useState(false);
119126
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
120127
const videoRef = useRef<HTMLVideoElement>(null);
@@ -237,11 +244,11 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, con
237244
<AccessibleTooltipButton
238245
className="mx_CallView_connectButton"
239246
kind="primary"
240-
disabled={connecting || Boolean(joinCallButtonDisabledTooltip)}
247+
disabled={connecting || joinCallButtonDisabled}
241248
onClick={onConnectClick}
242249
title={_t("Join")}
243250
label={_t("Join")}
244-
tooltip={connecting ? _t("Connecting") : joinCallButtonDisabledTooltip}
251+
tooltip={connecting ? _t("Connecting") : joinCallButtonTooltip}
245252
/>
246253
</div>;
247254
};
@@ -323,7 +330,8 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
323330
const cli = useContext(MatrixClientContext);
324331
const connected = isConnected(useConnectionState(call));
325332
const participants = useParticipants(call);
326-
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
333+
const joinCallButtonTooltip = useJoinCallButtonTooltip(call);
334+
const joinCallButtonDisabled = useJoinCallButtonDisabled(call);
327335

328336
const connect = useCallback(async () => {
329337
// Disconnect from any other active calls first, since we don't yet support holding
@@ -350,7 +358,8 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
350358
lobby = <Lobby
351359
room={room}
352360
connect={connect}
353-
joinCallButtonDisabledTooltip={joinCallButtonDisabledTooltip}
361+
joinCallButtonTooltip={joinCallButtonTooltip}
362+
joinCallButtonDisabled={joinCallButtonDisabled}
354363
>
355364
{ facePile }
356365
</Lobby>;

src/hooks/useCall.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { useState, useCallback } from "react";
17+
import { useState, useCallback, useMemo } from "react";
1818

1919
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
2020
import { Call, ConnectionState, ElementCall, Layout } from "../models/Call";
@@ -24,6 +24,7 @@ import { CallStore, CallStoreEvent } from "../stores/CallStore";
2424
import { useEventEmitter } from "./useEventEmitter";
2525
import SdkConfig, { DEFAULTS } from "../SdkConfig";
2626
import { _t } from "../languageHandler";
27+
import { MatrixClientPeg } from "../MatrixClientPeg";
2728

2829
export const useCall = (roomId: string): Call | null => {
2930
const [call, setCall] = useState(() => CallStore.instance.getCall(roomId));
@@ -56,15 +57,33 @@ export const useFull = (call: Call): boolean => {
5657
);
5758
};
5859

59-
export const useJoinCallButtonDisabledTooltip = (call: Call): string | null => {
60+
export const useIsAlreadyParticipant = (call: Call): boolean => {
61+
const client = MatrixClientPeg.get();
62+
const participants = useParticipants(call);
63+
64+
return useMemo(() => {
65+
return participants.has(client.getRoom(call.roomId).getMember(client.getUserId()));
66+
}, [participants, client, call]);
67+
};
68+
69+
export const useJoinCallButtonTooltip = (call: Call): string | null => {
6070
const isFull = useFull(call);
6171
const state = useConnectionState(call);
72+
const isAlreadyParticipant = useIsAlreadyParticipant(call);
6273

6374
if (state === ConnectionState.Connecting) return _t("Connecting");
6475
if (isFull) return _t("Sorry — this call is currently full");
76+
if (isAlreadyParticipant) return _t("You have already joined this call from another device");
6577
return null;
6678
};
6779

80+
export const useJoinCallButtonDisabled = (call: Call): boolean => {
81+
const isFull = useFull(call);
82+
const state = useConnectionState(call);
83+
84+
return isFull || state === ConnectionState.Connecting;
85+
};
86+
6887
export const useLayout = (call: ElementCall): Layout =>
6988
useTypedEventEmitterState(
7089
call,

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@
10231023
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
10241024
"Connecting": "Connecting",
10251025
"Sorry — this call is currently full": "Sorry — this call is currently full",
1026+
"You have already joined this call from another device": "You have already joined this call from another device",
10261027
"Create account": "Create account",
10271028
"You made it!": "You made it!",
10281029
"Find and invite your friends": "Find and invite your friends",

src/models/Call.ts

+33-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
1818
import { logger } from "matrix-js-sdk/src/logger";
1919
import { randomString } from "matrix-js-sdk/src/randomstring";
20-
import { MatrixClient } from "matrix-js-sdk/src/client";
20+
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
2121
import { RoomEvent } from "matrix-js-sdk/src/models/room";
2222
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
2323
import { CallType } from "matrix-js-sdk/src/webrtc/call";
@@ -606,8 +606,11 @@ export interface ElementCallMemberContent {
606606
export class ElementCall extends Call {
607607
public static readonly CALL_EVENT_TYPE = new NamespacedValue(null, "org.matrix.msc3401.call");
608608
public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, "org.matrix.msc3401.call.member");
609+
public static readonly DUPLICATE_CALL_DEVICE_EVENT_TYPE = "io.element.duplicate_call_device";
609610
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
610611

612+
private kickedOutByAnotherDevice = false;
613+
private connectionTime: number | null = null;
611614
private participantsExpirationTimer: number | null = null;
612615
private terminationTimer: number | null = null;
613616

@@ -785,6 +788,16 @@ export class ElementCall extends Call {
785788
audioInput: MediaDeviceInfo | null,
786789
videoInput: MediaDeviceInfo | null,
787790
): Promise<void> {
791+
this.kickedOutByAnotherDevice = false;
792+
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
793+
794+
this.connectionTime = Date.now();
795+
await this.client.sendToDevice(ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE, {
796+
[this.client.getUserId()]: {
797+
"*": { device_id: this.client.getDeviceId(), timestamp: this.connectionTime },
798+
},
799+
});
800+
788801
try {
789802
await this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
790803
audioInput: audioInput?.label ?? null,
@@ -808,6 +821,7 @@ export class ElementCall extends Call {
808821
}
809822

810823
public setDisconnected() {
824+
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
811825
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
812826
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
813827
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
@@ -845,8 +859,13 @@ export class ElementCall extends Call {
845859
}
846860

847861
private get mayTerminate(): boolean {
848-
return this.groupCall.getContent()["m.intent"] !== "m.room"
849-
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
862+
if (this.kickedOutByAnotherDevice) return false;
863+
if (this.groupCall.getContent()["m.intent"] === "m.room") return false;
864+
if (
865+
!this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client)
866+
) return false;
867+
868+
return true;
850869
}
851870

852871
private async terminate(): Promise<void> {
@@ -868,6 +887,17 @@ export class ElementCall extends Call {
868887
if ("m.terminated" in newGroupCall.getContent()) this.destroy();
869888
};
870889

890+
private onToDeviceEvent = (event: MatrixEvent): void => {
891+
const content = event.getContent();
892+
if (event.getType() !== ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE) return;
893+
if (event.getSender() !== this.client.getUserId()) return;
894+
if (content.device_id === this.client.getDeviceId()) return;
895+
if (content.timestamp <= this.connectionTime) return;
896+
897+
this.kickedOutByAnotherDevice = true;
898+
this.disconnect();
899+
};
900+
871901
private onConnectionState = (state: ConnectionState, prevState: ConnectionState) => {
872902
if (
873903
(state === ConnectionState.Connected && !isConnected(prevState))

src/toasts/IncomingCallToast.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
LiveContentSummaryWithCall,
3131
LiveContentType,
3232
} from "../components/views/rooms/LiveContentSummary";
33-
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
33+
import { useCall, useJoinCallButtonDisabled, useJoinCallButtonTooltip } from "../hooks/useCall";
3434
import { useRoomState } from "../hooks/useRoomState";
3535
import { ButtonEvent } from "../components/views/elements/AccessibleButton";
3636
import { useDispatcher } from "../hooks/useDispatcher";
@@ -45,12 +45,13 @@ interface JoinCallButtonWithCallProps {
4545
}
4646

4747
function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps) {
48-
const tooltip = useJoinCallButtonDisabledTooltip(call);
48+
const tooltip = useJoinCallButtonTooltip(call);
49+
const disabled = useJoinCallButtonDisabled(call);
4950

5051
return <AccessibleTooltipButton
5152
className="mx_IncomingCallToast_joinButton"
5253
onClick={onClick}
53-
disabled={Boolean(tooltip)}
54+
disabled={disabled}
5455
tooltip={tooltip}
5556
kind="primary"
5657
>

test/models/Call-test.ts

+74-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import EventEmitter from "events";
1818
import { mocked } from "jest-mock";
1919
import { waitFor } from "@testing-library/react";
2020
import { RoomType } from "matrix-js-sdk/src/@types/event";
21-
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
21+
import { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client";
2222
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
2323
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
2424
import { Widget } from "matrix-widget-api";
25+
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2526

2627
import type { Mocked } from "jest-mock";
2728
import type { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
@@ -85,6 +86,7 @@ const setUpClientRoomAndStores = (): {
8586
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
8687
client.getRooms.mockReturnValue([room]);
8788
client.getUserId.mockReturnValue(alice.userId);
89+
client.getDeviceId.mockReturnValue("alices_device");
8890
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
8991
client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => {
9092
if (roomId !== room.roomId) throw new Error("Unknown room");
@@ -814,6 +816,77 @@ describe("ElementCall", () => {
814816
call.off(CallEvent.Destroy, onDestroy);
815817
});
816818

819+
describe("being kicked out by another device", () => {
820+
const onDestroy = jest.fn();
821+
822+
beforeEach(async () => {
823+
await call.connect();
824+
call.on(CallEvent.Destroy, onDestroy);
825+
826+
jest.advanceTimersByTime(100);
827+
jest.clearAllMocks();
828+
});
829+
830+
afterEach(() => {
831+
call.off(CallEvent.Destroy, onDestroy);
832+
});
833+
834+
it("does not terminate the call if we are the last", async () => {
835+
client.emit(ClientEvent.ToDeviceEvent, {
836+
getType: () => (ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE),
837+
getContent: () => ({ device_id: "random_device_id", timestamp: Date.now() }),
838+
getSender: () => (client.getUserId()),
839+
} as MatrixEvent);
840+
841+
expect(client.sendStateEvent).not.toHaveBeenCalled();
842+
expect(
843+
[ConnectionState.Disconnecting, ConnectionState.Disconnected].includes(call.connectionState),
844+
).toBeTruthy();
845+
});
846+
847+
it("ignores messages from our device", async () => {
848+
client.emit(ClientEvent.ToDeviceEvent, {
849+
getSender: () => (client.getUserId()),
850+
getType: () => (ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE),
851+
getContent: () => ({ device_id: client.getDeviceId(), timestamp: Date.now() }),
852+
} as MatrixEvent);
853+
854+
expect(client.sendStateEvent).not.toHaveBeenCalled();
855+
expect(
856+
[ConnectionState.Disconnecting, ConnectionState.Disconnected].includes(call.connectionState),
857+
).toBeFalsy();
858+
expect(onDestroy).not.toHaveBeenCalled();
859+
});
860+
861+
it("ignores messages from other users", async () => {
862+
client.emit(ClientEvent.ToDeviceEvent, {
863+
getSender: () => (bob.userId),
864+
getType: () => (ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE),
865+
getContent: () => ({ device_id: "random_device_id", timestamp: Date.now() }),
866+
} as MatrixEvent);
867+
868+
expect(client.sendStateEvent).not.toHaveBeenCalled();
869+
expect(
870+
[ConnectionState.Disconnecting, ConnectionState.Disconnected].includes(call.connectionState),
871+
).toBeFalsy();
872+
expect(onDestroy).not.toHaveBeenCalled();
873+
});
874+
875+
it("ignores messages from the past", async () => {
876+
client.emit(ClientEvent.ToDeviceEvent, {
877+
getSender: () => (client.getUserId()),
878+
getType: () => (ElementCall.DUPLICATE_CALL_DEVICE_EVENT_TYPE),
879+
getContent: () => ({ device_id: "random_device_id", timestamp: 0 }),
880+
} as MatrixEvent);
881+
882+
expect(client.sendStateEvent).not.toHaveBeenCalled();
883+
expect(
884+
[ConnectionState.Disconnecting, ConnectionState.Disconnected].includes(call.connectionState),
885+
).toBeFalsy();
886+
expect(onDestroy).not.toHaveBeenCalled();
887+
});
888+
});
889+
817890
it("ends the call after a random delay if the last participant leaves without ending it", async () => {
818891
// Bob connects
819892
await client.sendStateEvent(

0 commit comments

Comments
 (0)