From b5d710a6ab8a1e22638fc4aefce2f7151a66d96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 15:43:52 +0200 Subject: [PATCH 01/32] Add `arrayFastClone` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/utils.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..cb022dc47 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020, 2021 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. +*/ + +/** + * Clones an array as fast as possible, retaining references of the array's values. + * @param a The array to clone. Must be defined. + * @returns A copy of the array. + */ +export function arrayFastClone(a: T[]): T[] { + return a.slice(0, a.length); +} From 1f72be47d0e577d18233a028902c4e65384ef7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:00:20 +0200 Subject: [PATCH 02/32] Add `Remove` icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/icons/Remove.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/icons/Remove.svg diff --git a/src/icons/Remove.svg b/src/icons/Remove.svg new file mode 100644 index 000000000..c9730ed61 --- /dev/null +++ b/src/icons/Remove.svg @@ -0,0 +1,4 @@ + + + + From e1b158675e0d5a8a08e4978257ea75e247610fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:01:07 +0200 Subject: [PATCH 03/32] Add `RemoveButton` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.module.css | 10 ++++++++++ src/button/Button.tsx | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/button/Button.module.css b/src/button/Button.module.css index d5cc8b15f..1ca6fc87a 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -220,6 +220,16 @@ limitations under the License. cursor: pointer; } +.removeButton { + width: 24px; + height: 24px; + + margin: 12px; + + background-color: var(--cpd-color-bg-subtle-secondary); + border-radius: 100%; +} + @media (hover: hover) { .toolbarButton:hover, .toolbarButtonSecondary:hover { diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 3d269dc28..1882809c7 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -32,6 +32,7 @@ import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-to import styles from "./Button.module.css"; import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg"; import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg"; +import { ReactComponent as RemoveIcon } from "../icons/Remove.svg"; import { VolumeIcon } from "./VolumeIcon"; export type ButtonVariant = @@ -260,6 +261,18 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) { ); } +export function RemoveButton({ ...rest }: Omit) { + const { t } = useTranslation(); + + return ( + + + + ); +} + interface FullscreenButtonProps extends Omit { fullscreen?: boolean; } From 430fca6cc893d8f35f0719d685ad143958d1778c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:02:55 +0200 Subject: [PATCH 04/32] Add basic `BreakoutRoomModal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.module.css | 43 +++++++ src/room/BreakoutRoomModal.tsx | 166 ++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 src/room/BreakoutRoomModal.module.css create mode 100644 src/room/BreakoutRoomModal.tsx diff --git a/src/room/BreakoutRoomModal.module.css b/src/room/BreakoutRoomModal.module.css new file mode 100644 index 000000000..aa282cfb5 --- /dev/null +++ b/src/room/BreakoutRoomModal.module.css @@ -0,0 +1,43 @@ +/* +Copyright 2023 Šimon Brandner + +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. +*/ + +.breakoutRooms { + max-block-size: 350px; + overflow-y: auto; + + margin-bottom: 24px; +} + +.breakoutRoomNameFieldRow { + display: flex; + flex-direction: row; + align-items: center; + + margin-top: 10px; + margin-bottom: 10px; + + width: 100%; +} + +.breakoutRoomNameField { + margin-right: 0; +} + +.breakoutRoomsButtons { + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx new file mode 100644 index 000000000..32b7fbc22 --- /dev/null +++ b/src/room/BreakoutRoomModal.tsx @@ -0,0 +1,166 @@ +/* +Copyright 2023 Šimon Brandner + +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 { ChangeEvent, useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MatrixClient } from "matrix-js-sdk"; +import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; +import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; + +import { Modal } from "../Modal"; +import { Button, RemoveButton } from "../button/Button"; +import { FieldRow, InputField } from "../input/Input"; +import { arrayFastClone } from "../utils"; +import styles from "./BreakoutRoomModal.module.css"; + +interface BreakoutRoom extends BreakoutRoomBase { + roomName: string; + roomId?: string; +} + +interface BreakoutRoomRowProps { + index: number; + roomName: string; + users: string[]; + onRoomNameChanged: (index: number, newRoomName: string) => void; + onUsersChanged: (index: number, newUsers: string[]) => void; + onRemove: (index: number) => void; +} + +const BreakoutRoomRow = ({ + index, + roomName, + users, + onRoomNameChanged, + onUsersChanged, + onRemove, +}: BreakoutRoomRowProps) => { + const { t } = useTranslation(); + + const onRoomNameFieldChange = useCallback( + (ev: ChangeEvent) => { + onRoomNameChanged(index, ev.currentTarget.value); + }, + [onRoomNameChanged, index] + ); + + const onRemoveClick = useCallback(() => { + onRemove(index); + }, [onRemove, index]); + + return ( + + + + + ); +}; + +interface Props { + client: MatrixClient; + roomId: string; + open: boolean; + onDismiss: () => void; +} + +export const BreakoutRoomModal = ({ + client, + roomId, + open, + onDismiss, +}: Props) => { + const { t } = useTranslation(); + + const [submitting, setSubmitting] = useState(false); + const [breakoutRooms, setBreakoutRooms] = useState(() => [ + { roomName: t("Break-out room 1"), users: [] }, + { roomName: t("Break-out room 2"), users: [] }, + ]); + + const onRoomNameChanged = useCallback( + (index: number, newRoomName: string) => { + const rooms = arrayFastClone(breakoutRooms); + rooms[index].roomName = newRoomName; + setBreakoutRooms(rooms); + }, + [breakoutRooms, setBreakoutRooms] + ); + + const onUsersChanged = useCallback( + (index: number, newUsers: string[]) => { + const rooms = arrayFastClone(breakoutRooms); + rooms[index].users = newUsers; + setBreakoutRooms(rooms); + }, + [breakoutRooms, setBreakoutRooms] + ); + + const onRemoveRoom = useCallback( + (index: number) => { + const rooms = arrayFastClone(breakoutRooms); + rooms.splice(index, 1); + setBreakoutRooms(rooms); + }, + [breakoutRooms, setBreakoutRooms] + ); + + const onAddBreakoutRoom = useCallback(() => { + const rooms = arrayFastClone(breakoutRooms); + rooms.push({ roomName: "", users: [] } as NewBreakoutRoom); + setBreakoutRooms(rooms); + }, [breakoutRooms, setBreakoutRooms]); + + const onSubmit = useCallback(async () => { + setSubmitting(true); + await client.createBreakoutRooms(roomId, breakoutRooms); + onDismiss(); + }, [client, roomId, breakoutRooms, onDismiss]); + + return ( + +
+ {breakoutRooms.map((r, index) => ( + + ))} +
+
+ + +
+
+ ); +}; From 7e3b05f7e5e1da16d43520c61750b22b0dfc217c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:06:11 +0200 Subject: [PATCH 05/32] Add `BreakoutRoom` icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/icons/BreakoutRoom.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/icons/BreakoutRoom.svg diff --git a/src/icons/BreakoutRoom.svg b/src/icons/BreakoutRoom.svg new file mode 100644 index 000000000..eee0603d6 --- /dev/null +++ b/src/icons/BreakoutRoom.svg @@ -0,0 +1,3 @@ + From 47f171c5131e2944718e864ef845b0a5cdca217d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:06:18 +0200 Subject: [PATCH 06/32] Add `BreakoutRoomButton` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 1882809c7..1ec5e280f 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -33,6 +33,7 @@ import styles from "./Button.module.css"; import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg"; import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg"; import { ReactComponent as RemoveIcon } from "../icons/Remove.svg"; +import { ReactComponent as BreakoutRoomIcon } from "../icons/BreakoutRoom.svg"; import { VolumeIcon } from "./VolumeIcon"; export type ButtonVariant = @@ -242,6 +243,23 @@ export function SettingsButton({ ); } +export function BreakoutRoomButton({ + ...rest +}: { + // TODO: add all props for + + ); +} + interface AudioButtonProps extends Omit { /** * A number between 0 and 1 From 0cda5fcf34571eb1d984daf76e467fd7680a19b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 26 Sep 2023 16:06:26 +0200 Subject: [PATCH 07/32] Present `BreakoutRoomButton` in UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/InCallView.tsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 707eb5d8c..b187c5a84 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -42,6 +42,7 @@ import { VideoButton, ScreenshareButton, SettingsButton, + BreakoutRoomButton, } from "../button"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { @@ -76,6 +77,7 @@ import { ECConnectionState, } from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; +import { BreakoutRoomModal } from "./BreakoutRoomModal"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -326,6 +328,18 @@ export function InCallView({ [setSettingsModalOpen] ); + const [breakoutRoomModalModalOpen, setBreakoutRoomModalModalOpen] = + useState(false); + + const openBreakoutRoomModal = useCallback( + () => setBreakoutRoomModalModalOpen(true), + [setBreakoutRoomModalModalOpen] + ); + const closeBreakoutRoomModal = useCallback( + () => setBreakoutRoomModalModalOpen(false), + [setBreakoutRoomModalModalOpen] + ); + const toggleScreensharing = useCallback(async () => { exitFullscreen(); await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, { @@ -371,7 +385,10 @@ export function InCallView({ /> ); } - buttons.push(); + buttons.push( + + ); + buttons.push(); } buttons.push( @@ -436,6 +453,12 @@ export function InCallView({ /> )*/} {!noControls && } + Date: Fri, 29 Sep 2023 09:42:39 +0200 Subject: [PATCH 08/32] Add new BreakoutRooms button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 24 ++++++++++++++++--- .../{BreakoutRoom.svg => AddBreakoutRoom.svg} | 0 src/icons/BreakoutRooms.svg | 3 +++ src/room/InCallView.tsx | 4 ++-- 4 files changed, 26 insertions(+), 5 deletions(-) rename src/icons/{BreakoutRoom.svg => AddBreakoutRoom.svg} (100%) create mode 100644 src/icons/BreakoutRooms.svg diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 1ec5e280f..9558770e8 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -33,7 +33,8 @@ import styles from "./Button.module.css"; import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg"; import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg"; import { ReactComponent as RemoveIcon } from "../icons/Remove.svg"; -import { ReactComponent as BreakoutRoomIcon } from "../icons/BreakoutRoom.svg"; +import { ReactComponent as AddBreakoutRoomIcon } from "../icons/AddBreakoutRoom.svg"; +import { ReactComponent as BreakoutRoomsIcon } from "../icons/BreakoutRooms.svg"; import { VolumeIcon } from "./VolumeIcon"; export type ButtonVariant = @@ -243,7 +244,7 @@ export function SettingsButton({ ); } -export function BreakoutRoomButton({ +export function AddBreakoutRoomButton({ ...rest }: { // TODO: add all props for + + ); +} + +export function BreakoutRoomsButton({ + ...rest +}: { + // TODO: add all props for ); diff --git a/src/icons/BreakoutRoom.svg b/src/icons/AddBreakoutRoom.svg similarity index 100% rename from src/icons/BreakoutRoom.svg rename to src/icons/AddBreakoutRoom.svg diff --git a/src/icons/BreakoutRooms.svg b/src/icons/BreakoutRooms.svg new file mode 100644 index 000000000..ba6315ac7 --- /dev/null +++ b/src/icons/BreakoutRooms.svg @@ -0,0 +1,3 @@ + diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b187c5a84..f11845f45 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -42,7 +42,7 @@ import { VideoButton, ScreenshareButton, SettingsButton, - BreakoutRoomButton, + AddBreakoutRoomButton, } from "../button"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { @@ -386,7 +386,7 @@ export function InCallView({ ); } buttons.push( - + ); buttons.push(); } From 65a2fab7fb2c55598a7d656b8fa05b7ede1381f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 1 Oct 2023 17:24:21 +0200 Subject: [PATCH 09/32] Make sure we show loading when joining a new room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/useLoadGroupCall.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index e29a7cb6c..a0ca5d8e8 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -55,12 +55,17 @@ export const useLoadGroupCall = ( viaServers: string[] ): GroupCallStatus => { const { t } = useTranslation(); + const [lastRoomId, setLastRoomId] = useState(); const [state, setState] = useState({ kind: "loading" }); const [e2eeEnabled] = useEnableE2EE(); useEffect(() => { const fetchOrCreateRoom = async (): Promise => { + if (lastRoomId !== roomIdOrAlias) { + setState({ kind: "loading" }); + } + let room: Room | null = null; if (roomIdOrAlias[0] === "#") { // We lowercase the localpart when we create the room, so we must lowercase @@ -127,9 +132,15 @@ export const useLoadGroupCall = ( waitForClientSyncing() .then(fetchOrCreateGroupCall) - .then((rtcSession) => setState({ kind: "loaded", rtcSession })) - .catch((error) => setState({ kind: "failed", error })); - }, [client, roomIdOrAlias, viaServers, t, e2eeEnabled]); + .then((rtcSession) => { + setLastRoomId(roomIdOrAlias); + setState({ kind: "loaded", rtcSession }); + }) + .catch((error) => { + setLastRoomId(roomIdOrAlias); + setState({ kind: "failed", error }); + }); + }, [client, roomIdOrAlias, viaServers, t, e2eeEnabled, lastRoomId]); return state; }; From 83f2b800cb73c62384e31737cd5485ccb737c44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 1 Oct 2023 17:38:09 +0200 Subject: [PATCH 10/32] Make sure to create a key for new breakout rooms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 32b7fbc22..fc936357f 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -19,12 +19,16 @@ import { useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk"; import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; +import { randomString } from "matrix-js-sdk/src/randomstring"; import { Modal } from "../Modal"; import { Button, RemoveButton } from "../button/Button"; import { FieldRow, InputField } from "../input/Input"; import { arrayFastClone } from "../utils"; import styles from "./BreakoutRoomModal.module.css"; +import { setLocalStorageItem } from "../useLocalStorage"; +import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; +import { useEnableE2EE } from "../settings/useSetting"; interface BreakoutRoom extends BreakoutRoomBase { roomName: string; @@ -92,6 +96,7 @@ export const BreakoutRoomModal = ({ onDismiss, }: Props) => { const { t } = useTranslation(); + const [e2eeEnabled] = useEnableE2EE(); const [submitting, setSubmitting] = useState(false); const [breakoutRooms, setBreakoutRooms] = useState(() => [ @@ -134,9 +139,20 @@ export const BreakoutRoomModal = ({ const onSubmit = useCallback(async () => { setSubmitting(true); - await client.createBreakoutRooms(roomId, breakoutRooms); + const { newRooms } = await client.createBreakoutRooms( + roomId, + breakoutRooms + ); + for (const room of newRooms) { + if (e2eeEnabled) { + setLocalStorageItem( + getRoomSharedKeyLocalStorageKey(room.roomId), + randomString(32) + ); + } + } onDismiss(); - }, [client, roomId, breakoutRooms, onDismiss]); + }, [client, roomId, breakoutRooms, e2eeEnabled, onDismiss]); return ( From e5838e1676de41d676aa5c26f611f75308ff6b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 1 Oct 2023 17:39:54 +0200 Subject: [PATCH 11/32] Add `BreakoutRoomsOverlay` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomsOverlay.module.css | 36 +++++++++ src/room/BreakoutRoomsOverlay.tsx | 94 ++++++++++++++++++++++++ src/room/InCallView.tsx | 2 + 3 files changed, 132 insertions(+) create mode 100644 src/room/BreakoutRoomsOverlay.module.css create mode 100644 src/room/BreakoutRoomsOverlay.tsx diff --git a/src/room/BreakoutRoomsOverlay.module.css b/src/room/BreakoutRoomsOverlay.module.css new file mode 100644 index 000000000..64b741a54 --- /dev/null +++ b/src/room/BreakoutRoomsOverlay.module.css @@ -0,0 +1,36 @@ +/* +Copyright 2023 Šimon Brandner + +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. +*/ + +.breakoutRoomsOverlay { + position: absolute; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + + height: 100%; + + gap: 8px; + right: 0; + + padding: 8px; +} + +.callTile { + height: auto; + width: min-content; +} diff --git a/src/room/BreakoutRoomsOverlay.tsx b/src/room/BreakoutRoomsOverlay.tsx new file mode 100644 index 000000000..278b04938 --- /dev/null +++ b/src/room/BreakoutRoomsOverlay.tsx @@ -0,0 +1,94 @@ +/* +Copyright 2023 Šimon Brandner + +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 { useCallback, useState } from "react"; +import { Room } from "matrix-js-sdk"; +import { BreakoutRoomsEvent } from "matrix-js-sdk/src/models/breakoutRooms"; +import { ExistingBreakoutRoomWithSummary } from "matrix-js-sdk/src/@types/breakout"; +import { Avatar } from "@vector-im/compound-web"; +import { Link } from "react-router-dom"; + +import { useTypedEventEmitter } from "../useEvents"; +import callListStyles from "../home/CallList.module.css"; +import styles from "./BreakoutRoomsOverlay.module.css"; +import { getRelativeRoomUrl } from "../matrix-utils"; +import { Body } from "../typography/Typography"; +import { Size } from "../Avatar"; +import { BreakoutRoomsButton } from "../button/Button"; + +interface Props { + room: Room; +} + +export const BreakoutRoomsOverlay = ({ room }: Props) => { + const [breakoutRooms, setBreakoutRooms] = useState(room?.getBreakoutRooms()); + const [expanded, setExpanded] = useState(false); + + const onExpandClick = useCallback(() => { + setExpanded(true); + }, [setExpanded]); + + const onCollapseClick = useCallback(() => { + setExpanded(false); + }, [setExpanded]); + + const onEvent = useCallback((rooms: ExistingBreakoutRoomWithSummary[]) => { + setBreakoutRooms(rooms); + setExpanded(true); + }, []); + + useTypedEventEmitter(room!, BreakoutRoomsEvent.RoomsChanged, onEvent); + + if (!breakoutRooms || breakoutRooms?.length === 0) { + return null; + } + + if (!expanded) + return ( +
+ +
+ ); + + return ( +
+ {breakoutRooms?.map((r) => ( +
+ + +
+ + {r.roomSummary.name} + +
+
+ +
+ ))} + +
+ ); +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index f11845f45..b970b9f98 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -78,6 +78,7 @@ import { } from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; import { BreakoutRoomModal } from "./BreakoutRoomModal"; +import { BreakoutRoomsOverlay } from "./BreakoutRoomsOverlay"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -465,6 +466,7 @@ export function InCallView({ open={settingsModalOpen} onDismiss={closeSettings} /> +
); } From bbe70b31850781454cf99e749a09ac658cd4bf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 1 Oct 2023 18:00:32 +0200 Subject: [PATCH 12/32] Hack to make rooms visible in home screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index fc936357f..0ae464f6e 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -16,7 +16,7 @@ limitations under the License. import { ChangeEvent, useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; -import { MatrixClient } from "matrix-js-sdk"; +import { GroupCallIntent, GroupCallType, MatrixClient } from "matrix-js-sdk"; import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; import { randomString } from "matrix-js-sdk/src/randomstring"; @@ -149,6 +149,13 @@ export const BreakoutRoomModal = ({ getRoomSharedKeyLocalStorageKey(room.roomId), randomString(32) ); + await client.createGroupCall( + room.roomId, + GroupCallType.Video, + false, + GroupCallIntent.Room, + true + ); } } onDismiss(); From af72a688a8616b48b8590821a5ea11ac64ea1843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 16 Oct 2023 14:22:25 +0200 Subject: [PATCH 13/32] Add `ButtonWithDropdown` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.module.css | 7 ++++++ src/button/Button.tsx | 44 +++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/button/Button.module.css b/src/button/Button.module.css index 1ca6fc87a..b7e4c9eb0 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -230,6 +230,13 @@ limitations under the License. border-radius: 100%; } +.buttonWithDropdown { + display: flex; + flex-direction: row; + + gap: 8px; +} + @media (hover: hover) { .toolbarButton:hover, .toolbarButtonSecondary:hover { diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 9558770e8..51cc1a7b5 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -13,7 +13,7 @@ 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 { forwardRef } from "react"; +import { forwardRef, useCallback, useState } from "react"; import { PressEvent } from "@react-types/shared"; import classNames from "classnames"; import { useButton } from "@react-aria/button"; @@ -28,6 +28,7 @@ import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg"; import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg"; import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg"; +import { ChangeEvent } from "react"; import styles from "./Button.module.css"; import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg"; @@ -138,6 +139,47 @@ export const Button = forwardRef( } ); +export function ButtonWithDropdown({ + label, + options, + onOptionSelect, + ...rest +}: { + label: string; + options: { label: string; id: string }[]; + onOptionSelect: (id: string) => void; +}) { + const [selectedUserId, setSelectedUserId] = useState(options[0].id); + + const onPress = useCallback(() => { + if (!selectedUserId) return; + + onOptionSelect(selectedUserId); + }, [onOptionSelect, selectedUserId]); + + const onSelectedOptionChange = useCallback( + (event: ChangeEvent) => { + setSelectedUserId(event.target.value); + }, + [setSelectedUserId] + ); + + return ( +
+ + + + +
+ ); +} + export function MicButton({ muted, ...rest From 5938ea65bbc3000dfe4ee771c2d6d3648186261d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 16 Oct 2023 14:25:14 +0200 Subject: [PATCH 14/32] Add ability to add users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.module.css | 16 ++++ src/room/BreakoutRoomModal.tsx | 130 ++++++++++++++++++++------ 2 files changed, 120 insertions(+), 26 deletions(-) diff --git a/src/room/BreakoutRoomModal.module.css b/src/room/BreakoutRoomModal.module.css index aa282cfb5..626db3f73 100644 --- a/src/room/BreakoutRoomModal.module.css +++ b/src/room/BreakoutRoomModal.module.css @@ -21,6 +21,12 @@ limitations under the License. margin-bottom: 24px; } +.breakoutRoom { + display: flex; + flex-direction: column; + width: 100%; +} + .breakoutRoomNameFieldRow { display: flex; flex-direction: row; @@ -36,6 +42,16 @@ limitations under the License. margin-right: 0; } +.breakoutRoomUser { + display: flex; + flex-direction: row; +} + +.breakoutRoomUser button { + margin: 0; + margin-left: 6px; +} + .breakoutRoomsButtons { display: flex; flex-direction: row; diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 0ae464f6e..3132547f5 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -14,15 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ChangeEvent, useCallback, useState } from "react"; +import { ChangeEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { GroupCallIntent, GroupCallType, MatrixClient } from "matrix-js-sdk"; +import { + GroupCallIntent, + GroupCallType, + MatrixClient, + RoomMember, +} from "matrix-js-sdk"; import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { Modal } from "../Modal"; -import { Button, RemoveButton } from "../button/Button"; +import { Button, ButtonWithDropdown, RemoveButton } from "../button/Button"; import { FieldRow, InputField } from "../input/Input"; import { arrayFastClone } from "../utils"; import styles from "./BreakoutRoomModal.module.css"; @@ -35,19 +40,44 @@ interface BreakoutRoom extends BreakoutRoomBase { roomId?: string; } +interface BreakoutRoomUserProps { + userId: string; + label: string; + onRemove: (id: string) => void; +} + +const BreakoutRoomUser = ({ + userId, + label, + onRemove, +}: BreakoutRoomUserProps) => { + const onRemoveButtonPress = useCallback(() => { + onRemove(userId); + }, [onRemove, userId]); + + return ( +
+ {label} + +
+ ); +}; + interface BreakoutRoomRowProps { - index: number; + roomIndex: number; roomName: string; - users: string[]; + members: RoomMember[]; + parentRoomMembers: RoomMember[]; onRoomNameChanged: (index: number, newRoomName: string) => void; onUsersChanged: (index: number, newUsers: string[]) => void; onRemove: (index: number) => void; } const BreakoutRoomRow = ({ - index, + roomIndex, roomName, - users, + members, + parentRoomMembers, onRoomNameChanged, onUsersChanged, onRemove, @@ -56,29 +86,71 @@ const BreakoutRoomRow = ({ const onRoomNameFieldChange = useCallback( (ev: ChangeEvent) => { - onRoomNameChanged(index, ev.currentTarget.value); + onRoomNameChanged(roomIndex, ev.currentTarget.value); }, - [onRoomNameChanged, index] + [onRoomNameChanged, roomIndex] ); const onRemoveClick = useCallback(() => { - onRemove(index); - }, [onRemove, index]); + onRemove(roomIndex); + }, [onRemove, roomIndex]); + + const onAddUser = useCallback( + (userId: string) => { + onUsersChanged(roomIndex, [...members.map((m) => m.userId), userId]); + }, + [onUsersChanged, roomIndex, members] + ); + + const onRemoveUser = useCallback( + (userId: string) => { + onUsersChanged( + roomIndex, + members.filter((m) => m.userId !== userId).map((m) => m.userId) + ); + }, + [onUsersChanged, roomIndex, members] + ); return ( - - - - +
+ + + + +
+ {members.map((m) => ( + + ))} + {parentRoomMembers.find( + (rm) => !members.find((m) => rm.userId === m.userId) + ) && ( + !members.includes(m)) + .map((m) => ({ + label: m.name, + id: m.userId, + }))} + onOptionSelect={onAddUser} + /> + )} +
+
); }; @@ -98,6 +170,9 @@ export const BreakoutRoomModal = ({ const { t } = useTranslation(); const [e2eeEnabled] = useEnableE2EE(); + const room = useMemo(() => client.getRoom(roomId), [client, roomId]); + const roomMembers = useMemo(() => room?.getMembers() ?? [], [room]); + const [submitting, setSubmitting] = useState(false); const [breakoutRooms, setBreakoutRooms] = useState(() => [ { roomName: t("Break-out room 1"), users: [] }, @@ -167,9 +242,12 @@ export const BreakoutRoomModal = ({ {breakoutRooms.map((r, index) => ( room?.getMember(u)) as RoomMember[] + ).filter((m) => !!m)} + parentRoomMembers={roomMembers} onRoomNameChanged={onRoomNameChanged} onUsersChanged={onUsersChanged} onRemove={onRemoveRoom} From c8183d4c1fd82e483eefb988d6debeafd4c05b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 17 Oct 2023 08:01:20 +0200 Subject: [PATCH 15/32] Display users in overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And fix some bugs Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomsOverlay.module.css | 28 +++++++++++++++++++++++ src/room/BreakoutRoomsOverlay.tsx | 29 ++++++++++++++---------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/room/BreakoutRoomsOverlay.module.css b/src/room/BreakoutRoomsOverlay.module.css index 64b741a54..edf218374 100644 --- a/src/room/BreakoutRoomsOverlay.module.css +++ b/src/room/BreakoutRoomsOverlay.module.css @@ -34,3 +34,31 @@ limitations under the License. height: auto; width: min-content; } + +.callTileLink { + display: flex; + flex-direction: column; +} + +.callTileHead { + display: flex; + text-decoration: none; + width: 100%; + height: 100%; + align-items: center; +} + +.callTileUsers { + display: flex; + flex-direction: column; + color: var(--cpd-color-text-primary); +} + +.callInfo { + padding: 0 8px; +} + +.callTileAvatar { + width: 24px; + height: 24px; +} diff --git a/src/room/BreakoutRoomsOverlay.tsx b/src/room/BreakoutRoomsOverlay.tsx index 278b04938..29cd6e896 100644 --- a/src/room/BreakoutRoomsOverlay.tsx +++ b/src/room/BreakoutRoomsOverlay.tsx @@ -71,20 +71,25 @@ export const BreakoutRoomsOverlay = ({ room }: Props) => { // note we explicitly omit the password here as we don't want it on this link because // it's just for the user to navigate around and not for sharing to={getRelativeRoomUrl(r.roomId, room.name)} - className={callListStyles.callTileLink} + className={styles.callTileLink} > - -
- - {r.roomSummary.name} - +
+ +
+ + {r.roomSummary.name} + +
+
+
+ {r.users.map((u) => room.getMember(u)?.name)}
-
))} From 1cd0545fc77729983b2ac385ae9f6355bf1f09c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 10:29:25 +0200 Subject: [PATCH 16/32] Update-js sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 434140026..9b596fc7c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "i18next-http-backend": "^2.0.0", "livekit-client": "^1.12.3", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#c8f8fb587d29dce22d314bfc16bf25a76b04e8bb", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#5802c00cf2786bf34630987b40b99cc8b625ec65", "matrix-widget-api": "^1.3.1", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/yarn.lock b/yarn.lock index fb59d2b80..c5eac431b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1992,10 +1992,10 @@ clsx "^2.0.0" usehooks-ts "^2.9.1" -"@matrix-org/matrix-sdk-crypto-wasm@^1.2.3-alpha.0": - version "1.2.3-alpha.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-1.2.3-alpha.0.tgz#f6f93e3ee44c5f1e0e255badd26f4a7d3fb1dab8" - integrity sha512-BFLqfq/WbYZ+83r4UWLhwtBYvTp5DKTHNeWUSDBVvudFtqBvkntNAAUz+xmhmO1XkyNm+sBaElxF8IS9S8zdww== +"@matrix-org/matrix-sdk-crypto-wasm@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-2.0.0.tgz#a4e9682705f090c94a58f6b851054f7598de9e83" + integrity sha512-8tKmI9u35URvtAr6zcfNGphImWt1y458iKq2PSPOSARlsmVk2lkrhsBFihBnWJY1oJC2EMsyfI8XTRuVYv00TQ== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -7252,12 +7252,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#c8f8fb587d29dce22d314bfc16bf25a76b04e8bb": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#5802c00cf2786bf34630987b40b99cc8b625ec65": version "29.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f8fb587d29dce22d314bfc16bf25a76b04e8bb" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/5802c00cf2786bf34630987b40b99cc8b625ec65" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^1.2.3-alpha.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^2.0.0" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 4631a7ddb5a62840e9bbbafaed790c131831c46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 11:06:47 +0200 Subject: [PATCH 17/32] Post-merge fix-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrix-utils.ts | 2 +- src/room/BreakoutRoomModal.tsx | 48 ++++++++++++++++------------------ 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 52c559594..42b71162f 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -73,7 +73,7 @@ function waitForSync(client: MatrixClient): Promise { }); } -function secureRandomString(entropyBytes: number): string { +export function secureRandomString(entropyBytes: number): string { const key = new Uint8Array(entropyBytes); crypto.getRandomValues(key); // encode to base64url as this value goes into URLs diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 3132547f5..686bae504 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -24,7 +24,6 @@ import { } from "matrix-js-sdk"; import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; -import { randomString } from "matrix-js-sdk/src/randomstring"; import { Modal } from "../Modal"; import { Button, ButtonWithDropdown, RemoveButton } from "../button/Button"; @@ -33,7 +32,7 @@ import { arrayFastClone } from "../utils"; import styles from "./BreakoutRoomModal.module.css"; import { setLocalStorageItem } from "../useLocalStorage"; import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; -import { useEnableE2EE } from "../settings/useSetting"; +import { secureRandomString } from "../matrix-utils"; interface BreakoutRoom extends BreakoutRoomBase { roomName: string; @@ -88,7 +87,7 @@ const BreakoutRoomRow = ({ (ev: ChangeEvent) => { onRoomNameChanged(roomIndex, ev.currentTarget.value); }, - [onRoomNameChanged, roomIndex] + [onRoomNameChanged, roomIndex], ); const onRemoveClick = useCallback(() => { @@ -99,17 +98,17 @@ const BreakoutRoomRow = ({ (userId: string) => { onUsersChanged(roomIndex, [...members.map((m) => m.userId), userId]); }, - [onUsersChanged, roomIndex, members] + [onUsersChanged, roomIndex, members], ); const onRemoveUser = useCallback( (userId: string) => { onUsersChanged( roomIndex, - members.filter((m) => m.userId !== userId).map((m) => m.userId) + members.filter((m) => m.userId !== userId).map((m) => m.userId), ); }, - [onUsersChanged, roomIndex, members] + [onUsersChanged, roomIndex, members], ); return ( @@ -136,7 +135,7 @@ const BreakoutRoomRow = ({ /> ))} {parentRoomMembers.find( - (rm) => !members.find((m) => rm.userId === m.userId) + (rm) => !members.find((m) => rm.userId === m.userId), ) && ( { const { t } = useTranslation(); - const [e2eeEnabled] = useEnableE2EE(); const room = useMemo(() => client.getRoom(roomId), [client, roomId]); const roomMembers = useMemo(() => room?.getMembers() ?? [], [room]); @@ -185,7 +183,7 @@ export const BreakoutRoomModal = ({ rooms[index].roomName = newRoomName; setBreakoutRooms(rooms); }, - [breakoutRooms, setBreakoutRooms] + [breakoutRooms, setBreakoutRooms], ); const onUsersChanged = useCallback( @@ -194,7 +192,7 @@ export const BreakoutRoomModal = ({ rooms[index].users = newUsers; setBreakoutRooms(rooms); }, - [breakoutRooms, setBreakoutRooms] + [breakoutRooms, setBreakoutRooms], ); const onRemoveRoom = useCallback( @@ -203,7 +201,7 @@ export const BreakoutRoomModal = ({ rooms.splice(index, 1); setBreakoutRooms(rooms); }, - [breakoutRooms, setBreakoutRooms] + [breakoutRooms, setBreakoutRooms], ); const onAddBreakoutRoom = useCallback(() => { @@ -216,25 +214,23 @@ export const BreakoutRoomModal = ({ setSubmitting(true); const { newRooms } = await client.createBreakoutRooms( roomId, - breakoutRooms + breakoutRooms, ); for (const room of newRooms) { - if (e2eeEnabled) { - setLocalStorageItem( - getRoomSharedKeyLocalStorageKey(room.roomId), - randomString(32) - ); - await client.createGroupCall( - room.roomId, - GroupCallType.Video, - false, - GroupCallIntent.Room, - true - ); - } + setLocalStorageItem( + getRoomSharedKeyLocalStorageKey(room.roomId), + secureRandomString(16), + ); + await client.createGroupCall( + room.roomId, + GroupCallType.Video, + false, + GroupCallIntent.Room, + true, + ); } onDismiss(); - }, [client, roomId, breakoutRooms, e2eeEnabled, onDismiss]); + }, [client, roomId, breakoutRooms, onDismiss]); return ( From 099b7ff51e38ebc09cedda45874d0e68d073cd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 11:45:05 +0200 Subject: [PATCH 18/32] Post-merge fix-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 5a242b77b..fe80e9b60 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -34,7 +34,7 @@ import { VolumeIcon } from "./VolumeIcon"; import styles from "./Button.module.css"; import Fullscreen from "../icons/Fullscreen.svg?react"; import FullscreenExit from "../icons/FullscreenExit.svg?react"; -import RemoveIcon from "../icons/Remove.svg"; +import RemoveIcon from "../icons/Remove.svg?react"; import AddBreakoutRoomIcon from "../icons/AddBreakoutRoom.svg?react"; import BreakoutRoomsIcon from "../icons/BreakoutRooms.svg?react"; From 90e2acd22485fc8764434e1f031591d7783711e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 11:49:53 +0200 Subject: [PATCH 19/32] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 10 +++++----- src/room/InCallView.tsx | 6 +++--- src/room/useLoadGroupCall.ts | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index fe80e9b60..f2ea51080 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -98,12 +98,12 @@ export const Button = forwardRef( onPressStart, ...rest }, - ref + ref, ) => { const buttonRef = useObjectRef(ref); const { buttonProps } = useButton( { onPress, onPressStart, ...rest }, - buttonRef + buttonRef, ); // TODO: react-aria's useButton hook prevents form submission via keyboard @@ -125,7 +125,7 @@ export const Button = forwardRef( { [styles.on]: on, [styles.off]: off, - } + }, )} {...mergeProps(rest, filteredButtonProps)} ref={buttonRef} @@ -136,7 +136,7 @@ export const Button = forwardRef( ); - } + }, ); export const ButtonWithDropdown = ({ @@ -161,7 +161,7 @@ export const ButtonWithDropdown = ({ (event: ChangeEvent) => { setSelectedUserId(event.target.value); }, - [setSelectedUserId] + [setSelectedUserId], ); return ( diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 8877ea440..e64be3d12 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -339,11 +339,11 @@ export const InCallView: FC = ({ const openBreakoutRoomModal = useCallback( () => setBreakoutRoomModalModalOpen(true), - [setBreakoutRoomModalModalOpen] + [setBreakoutRoomModalModalOpen], ); const closeBreakoutRoomModal = useCallback( () => setBreakoutRoomModalModalOpen(false), - [setBreakoutRoomModalModalOpen] + [setBreakoutRoomModalModalOpen], ); const toggleScreensharing = useCallback(async () => { @@ -392,7 +392,7 @@ export const InCallView: FC = ({ ); } buttons.push( - + , ); buttons.push(); } diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 3c1504d33..a49116fb8 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -51,7 +51,7 @@ export interface GroupCallLoadState { export const useLoadGroupCall = ( client: MatrixClient, roomIdOrAlias: string, - viaServers: string[] + viaServers: string[], ): GroupCallStatus => { const { t } = useTranslation(); const [lastRoomId, setLastRoomId] = useState(); @@ -72,7 +72,7 @@ export const useLoadGroupCall = ( // join anyway but the js-sdk recreates the room if you pass the alias for a // room you're already joined to (which it probably ought not to). const lookupResult = await client.getRoomIdForAlias( - roomIdOrAlias.toLowerCase() + roomIdOrAlias.toLowerCase(), ); logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`); room = client.getRoom(lookupResult.room_id); @@ -83,7 +83,7 @@ export const useLoadGroupCall = ( }); } else { logger.info( - `Already in room ${lookupResult.room_id}, not rejoining.` + `Already in room ${lookupResult.room_id}, not rejoining.`, ); } } else { @@ -94,7 +94,7 @@ export const useLoadGroupCall = ( } logger.info( - `Joined ${roomIdOrAlias}, waiting room to be ready for group calls` + `Joined ${roomIdOrAlias}, waiting room to be ready for group calls`, ); await client.waitUntilRoomReadyForGroupCalls(room.roomId); logger.info(`${roomIdOrAlias}, is ready for group calls`); @@ -112,7 +112,7 @@ export const useLoadGroupCall = ( const waitForClientSyncing = async (): Promise => { if (client.getSyncState() !== SyncState.Syncing) { logger.debug( - "useLoadGroupCall: waiting for client to start syncing..." + "useLoadGroupCall: waiting for client to start syncing...", ); await new Promise((resolve) => { const onSync = (): void => { From c66c1bc72ca58efc493265047c6390f323f1e2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 12:02:03 +0200 Subject: [PATCH 20/32] Simplify break-out room handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- src/room/BreakoutRoomModal.tsx | 43 ++++++++++--------------------- src/room/BreakoutRoomsOverlay.tsx | 4 +-- yarn.lock | 4 +-- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 16bf932fb..4a0ab9ad7 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "i18next-http-backend": "^2.0.0", "livekit-client": "^1.12.3", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0ccd169dffbcaee0a977170e28490082e29b7fa4", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8b7ea68fdfec35e44278a45bb3b8780e9fd6356f", "matrix-widget-api": "^1.3.1", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 686bae504..16e239e98 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -16,27 +16,19 @@ limitations under the License. import { ChangeEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - GroupCallIntent, - GroupCallType, - MatrixClient, - RoomMember, -} from "matrix-js-sdk"; -import { BreakoutRoomBase } from "matrix-js-sdk/src/@types/breakout"; -import { NewBreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; +import { MatrixClient, RoomMember } from "matrix-js-sdk"; +import { BreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; import { Modal } from "../Modal"; import { Button, ButtonWithDropdown, RemoveButton } from "../button/Button"; import { FieldRow, InputField } from "../input/Input"; import { arrayFastClone } from "../utils"; import styles from "./BreakoutRoomModal.module.css"; -import { setLocalStorageItem } from "../useLocalStorage"; -import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; -import { secureRandomString } from "../matrix-utils"; +import { createRoom } from "../matrix-utils"; -interface BreakoutRoom extends BreakoutRoomBase { +interface NewBreakoutRoom { roomName: string; - roomId?: string; + users: string[]; } interface BreakoutRoomUserProps { @@ -172,7 +164,7 @@ export const BreakoutRoomModal = ({ const roomMembers = useMemo(() => room?.getMembers() ?? [], [room]); const [submitting, setSubmitting] = useState(false); - const [breakoutRooms, setBreakoutRooms] = useState(() => [ + const [breakoutRooms, setBreakoutRooms] = useState(() => [ { roomName: t("Break-out room 1"), users: [] }, { roomName: t("Break-out room 2"), users: [] }, ]); @@ -212,23 +204,14 @@ export const BreakoutRoomModal = ({ const onSubmit = useCallback(async () => { setSubmitting(true); - const { newRooms } = await client.createBreakoutRooms( - roomId, - breakoutRooms, - ); - for (const room of newRooms) { - setLocalStorageItem( - getRoomSharedKeyLocalStorageKey(room.roomId), - secureRandomString(16), - ); - await client.createGroupCall( - room.roomId, - GroupCallType.Video, - false, - GroupCallIntent.Room, - true, - ); + + const newBreakoutRooms: BreakoutRoom[] = []; + for (const breakoutRoom of breakoutRooms) { + const { roomId } = await createRoom(client, breakoutRoom.roomName, true); + newBreakoutRooms.push({ roomId, users: breakoutRoom.users }); } + await client.createBreakoutRooms(roomId, newBreakoutRooms); + onDismiss(); }, [client, roomId, breakoutRooms, onDismiss]); diff --git a/src/room/BreakoutRoomsOverlay.tsx b/src/room/BreakoutRoomsOverlay.tsx index 29cd6e896..5312ea3db 100644 --- a/src/room/BreakoutRoomsOverlay.tsx +++ b/src/room/BreakoutRoomsOverlay.tsx @@ -17,7 +17,7 @@ limitations under the License. import { useCallback, useState } from "react"; import { Room } from "matrix-js-sdk"; import { BreakoutRoomsEvent } from "matrix-js-sdk/src/models/breakoutRooms"; -import { ExistingBreakoutRoomWithSummary } from "matrix-js-sdk/src/@types/breakout"; +import { BreakoutRoomWithSummary } from "matrix-js-sdk/src/@types/breakout"; import { Avatar } from "@vector-im/compound-web"; import { Link } from "react-router-dom"; @@ -45,7 +45,7 @@ export const BreakoutRoomsOverlay = ({ room }: Props) => { setExpanded(false); }, [setExpanded]); - const onEvent = useCallback((rooms: ExistingBreakoutRoomWithSummary[]) => { + const onEvent = useCallback((rooms: BreakoutRoomWithSummary[]) => { setBreakoutRooms(rooms); setExpanded(true); }, []); diff --git a/yarn.lock b/yarn.lock index c2b2a934f..807b5bc59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7042,9 +7042,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#0ccd169dffbcaee0a977170e28490082e29b7fa4": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8b7ea68fdfec35e44278a45bb3b8780e9fd6356f": version "29.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0ccd169dffbcaee0a977170e28490082e29b7fa4" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8b7ea68fdfec35e44278a45bb3b8780e9fd6356f" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^2.0.0" From a59ab952443f31b054ee536295cab10dfe7ac644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 13:47:10 +0200 Subject: [PATCH 21/32] Share break-out room keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ClientContext.tsx | 23 +++++++++++++++++++++++ src/e2ee/sharedKeyManagement.ts | 3 +++ src/room/BreakoutRoomModal.tsx | 26 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 9e47a6246..0b5a5c9a5 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -44,6 +44,12 @@ import { import { translatedError } from "./TranslatedError"; import { useEventTarget } from "./useEvents"; import { Config } from "./config/Config"; +import { MatrixEvent } from "matrix-js-sdk"; +import { + SHARE_ROOM_KEY_EVENT_TYPE, + getRoomSharedKeyLocalStorageKey, +} from "./e2ee/sharedKeyManagement"; +import { getLocalStorageItem, setLocalStorageItem } from "./useLocalStorage"; declare global { interface Window { @@ -304,6 +310,18 @@ export const ClientProvider: FC = ({ children }) => { [], ); + const onToDevice = useCallback((event: MatrixEvent) => { + if (event.getType() !== SHARE_ROOM_KEY_EVENT_TYPE) return; + + for (const [roomId, key] of Object.entries(event.getContent())) { + // XXX: We need a better way to prevent injection attacks here! + const localStorageKey = getRoomSharedKeyLocalStorageKey(roomId); + if (getLocalStorageItem(localStorageKey)) continue; + + setLocalStorageItem(localStorageKey, key); + } + }, []); + useEffect(() => { if (!initClientState) { return; @@ -317,11 +335,16 @@ export const ClientProvider: FC = ({ children }) => { if (initClientState.client) { initClientState.client.on(ClientEvent.Sync, onSync); + initClientState.client.on(ClientEvent.ToDeviceEvent, onToDevice); } return () => { if (initClientState.client) { initClientState.client.removeListener(ClientEvent.Sync, onSync); + initClientState.client.removeListener( + ClientEvent.ToDeviceEvent, + onToDevice, + ); } }; }, [initClientState, onSync]); diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index bc34f673c..6dc14d062 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -21,6 +21,9 @@ import { useClient } from "../ClientContext"; import { useUrlParams } from "../UrlParams"; import { widget } from "../widget"; +export type ShareRoomKeyEventContent = Record; +export const SHARE_ROOM_KEY_EVENT_TYPE = "io.element.share_room_key"; + export const getRoomSharedKeyLocalStorageKey = (roomId: string): string => `room-shared-key-${roomId}`; diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 16e239e98..2d543c354 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -25,6 +25,12 @@ import { FieldRow, InputField } from "../input/Input"; import { arrayFastClone } from "../utils"; import styles from "./BreakoutRoomModal.module.css"; import { createRoom } from "../matrix-utils"; +import { getLocalStorageItem } from "../useLocalStorage"; +import { + SHARE_ROOM_KEY_EVENT_TYPE, + ShareRoomKeyEventContent, + getRoomSharedKeyLocalStorageKey, +} from "../e2ee/sharedKeyManagement"; interface NewBreakoutRoom { roomName: string; @@ -205,11 +211,31 @@ export const BreakoutRoomModal = ({ const onSubmit = useCallback(async () => { setSubmitting(true); + const roomMembers = room?.getMembers(); + + const content: ShareRoomKeyEventContent = {}; const newBreakoutRooms: BreakoutRoom[] = []; for (const breakoutRoom of breakoutRooms) { const { roomId } = await createRoom(client, breakoutRoom.roomName, true); + const key = getLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId)); + if (key) { + content[roomId] = key; + } newBreakoutRooms.push({ roomId, users: breakoutRoom.users }); } + + if (roomMembers) { + const contentMap = new Map(); + for (const roomMember of roomMembers) { + if (roomMember.userId === client.getUserId()) continue; + + const deviceMap = new Map(); + deviceMap.set("*", content); + contentMap.set(roomMember.userId, deviceMap); + } + client.sendToDevice(SHARE_ROOM_KEY_EVENT_TYPE, contentMap); + } + await client.createBreakoutRooms(roomId, newBreakoutRooms); onDismiss(); From 95547f1ec1be0835a3d68f9496da6bd56c6fe685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 13:51:55 +0200 Subject: [PATCH 22/32] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ClientContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 0b5a5c9a5..f6d075944 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -29,6 +29,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { useTranslation } from "react-i18next"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; +import { MatrixEvent } from "matrix-js-sdk"; import { ErrorView } from "./FullScreenView"; import { @@ -44,7 +45,6 @@ import { import { translatedError } from "./TranslatedError"; import { useEventTarget } from "./useEvents"; import { Config } from "./config/Config"; -import { MatrixEvent } from "matrix-js-sdk"; import { SHARE_ROOM_KEY_EVENT_TYPE, getRoomSharedKeyLocalStorageKey, From 088706517f579e82f7474c54707e3861d1f7e87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 13:52:02 +0200 Subject: [PATCH 23/32] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index b09179e18..4744dc757 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -13,10 +13,16 @@ "<0>Thanks for your feedback!": "<0>Thanks for your feedback!", "<0>We'd love to hear your feedback so we can improve your experience.": "<0>We'd love to hear your feedback so we can improve your experience.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls", + "Add break-out room": "Add break-out room", + "Add user": "Add user", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.", "Audio": "Audio", "Avatar": "Avatar", "Back to recents": "Back to recents", + "Break-out room": "Break-out room", + "Break-out room 1": "Break-out room 1", + "Break-out room 2": "Break-out room 2", + "Break-out rooms": "Break-out rooms", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)", "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.", @@ -31,6 +37,8 @@ "Copy": "Copy", "Copy link": "Copy link", "Create account": "Create account", + "Create rooms": "Create rooms", + "Creating rooms...": "Creating rooms...", "Debug log request": "Debug log request", "Developer": "Developer", "Developer Settings": "Developer Settings", @@ -83,6 +91,7 @@ "Remove": "Remove", "Retry sending logs": "Retry sending logs", "Return to home screen": "Return to home screen", + "Room name": "Room name", "Select an option": "Select an option", "Select app": "Select app", "Send debug logs": "Send debug logs", From 61d8927e51537e8431034e9442cc8fbb85e95124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 13:59:37 +0200 Subject: [PATCH 24/32] Update js-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4a0ab9ad7..30d334feb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "i18next-http-backend": "^2.0.0", "livekit-client": "^1.12.3", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8b7ea68fdfec35e44278a45bb3b8780e9fd6356f", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#2eb43a1e9406e37528d01d84dfcc0a7e0a093e9e", "matrix-widget-api": "^1.3.1", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/yarn.lock b/yarn.lock index 807b5bc59..82b765617 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7042,9 +7042,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8b7ea68fdfec35e44278a45bb3b8780e9fd6356f": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#2eb43a1e9406e37528d01d84dfcc0a7e0a093e9e": version "29.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8b7ea68fdfec35e44278a45bb3b8780e9fd6356f" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2eb43a1e9406e37528d01d84dfcc0a7e0a093e9e" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^2.0.0" From 16bdcca1e957d731b3de9e0e1643427994f986e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 14:12:08 +0200 Subject: [PATCH 25/32] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ClientContext.tsx | 2 +- src/button/Button.tsx | 29 ++++++++++------------------- src/room/BreakoutRoomModal.tsx | 16 ++++++++-------- src/room/BreakoutRoomsOverlay.tsx | 4 ++-- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index f6d075944..5b8de4d3c 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -347,7 +347,7 @@ export const ClientProvider: FC = ({ children }) => { ); } }; - }, [initClientState, onSync]); + }, [initClientState, onSync, onToDevice]); if (alreadyOpenedErr) { return ; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index f2ea51080..2fe57762d 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -139,16 +139,11 @@ export const Button = forwardRef( }, ); -export const ButtonWithDropdown = ({ - label, - options, - onOptionSelect, - ...rest -}: { +export const ButtonWithDropdown: FC<{ label: string; options: { label: string; id: string }[]; onOptionSelect: (id: string) => void; -}) => { +}> = ({ label, options, onOptionSelect, ...rest }) => { const [selectedUserId, setSelectedUserId] = useState(options[0].id); const onPress = useCallback(() => { @@ -270,12 +265,10 @@ export const SettingsButton: FC<{ ); }; -export function AddBreakoutRoomButton({ - ...rest -}: { +export const AddBreakoutRoomButton: FC<{ // TODO: add all props for ); -} +}; -export function BreakoutRoomsButton({ - ...rest -}: { +export const BreakoutRoomsButton: FC<{ // TODO: add all props for ); -} +}; interface AudioButtonProps extends Omit { /** @@ -323,7 +314,7 @@ export const AudioButton: FC = ({ volume, ...rest }) => { ); }; -export function RemoveButton({ ...rest }: Omit) { +export const RemoveButton: FC> = ({ ...rest }) => { const { t } = useTranslation(); return ( @@ -333,7 +324,7 @@ export function RemoveButton({ ...rest }: Omit) { ); -} +}; interface FullscreenButtonProps extends Omit { fullscreen?: boolean; diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 2d543c354..b86d93521 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ChangeEvent, useCallback, useMemo, useState } from "react"; +import { ChangeEvent, FC, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { MatrixClient, RoomMember } from "matrix-js-sdk"; import { BreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; @@ -43,11 +43,11 @@ interface BreakoutRoomUserProps { onRemove: (id: string) => void; } -const BreakoutRoomUser = ({ +const BreakoutRoomUser: FC = ({ userId, label, onRemove, -}: BreakoutRoomUserProps) => { +}) => { const onRemoveButtonPress = useCallback(() => { onRemove(userId); }, [onRemove, userId]); @@ -70,7 +70,7 @@ interface BreakoutRoomRowProps { onRemove: (index: number) => void; } -const BreakoutRoomRow = ({ +const BreakoutRoomRow: FC = ({ roomIndex, roomName, members, @@ -78,7 +78,7 @@ const BreakoutRoomRow = ({ onRoomNameChanged, onUsersChanged, onRemove, -}: BreakoutRoomRowProps) => { +}) => { const { t } = useTranslation(); const onRoomNameFieldChange = useCallback( @@ -158,12 +158,12 @@ interface Props { onDismiss: () => void; } -export const BreakoutRoomModal = ({ +export const BreakoutRoomModal: FC = ({ client, roomId, open, onDismiss, -}: Props) => { +}) => { const { t } = useTranslation(); const room = useMemo(() => client.getRoom(roomId), [client, roomId]); @@ -239,7 +239,7 @@ export const BreakoutRoomModal = ({ await client.createBreakoutRooms(roomId, newBreakoutRooms); onDismiss(); - }, [client, roomId, breakoutRooms, onDismiss]); + }, [client, roomId, room, breakoutRooms, onDismiss]); return ( diff --git a/src/room/BreakoutRoomsOverlay.tsx b/src/room/BreakoutRoomsOverlay.tsx index 5312ea3db..ed2882923 100644 --- a/src/room/BreakoutRoomsOverlay.tsx +++ b/src/room/BreakoutRoomsOverlay.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useState } from "react"; +import { FC, useCallback, useState } from "react"; import { Room } from "matrix-js-sdk"; import { BreakoutRoomsEvent } from "matrix-js-sdk/src/models/breakoutRooms"; import { BreakoutRoomWithSummary } from "matrix-js-sdk/src/@types/breakout"; @@ -33,7 +33,7 @@ interface Props { room: Room; } -export const BreakoutRoomsOverlay = ({ room }: Props) => { +export const BreakoutRoomsOverlay: FC = ({ room }) => { const [breakoutRooms, setBreakoutRooms] = useState(room?.getBreakoutRooms()); const [expanded, setExpanded] = useState(false); From b6b291154b9f2c10dd7434e08cac98d06750e851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 14:57:02 +0200 Subject: [PATCH 26/32] Fix `ButtonWithDropdown` behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 12 ++++++++---- src/room/BreakoutRoomModal.tsx | 26 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 2fe57762d..4cd0d66d2 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -13,7 +13,7 @@ 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 { FC, forwardRef, useCallback, useState } from "react"; +import { FC, forwardRef, useCallback, useEffect, useState } from "react"; import { PressEvent } from "@react-types/shared"; import classNames from "classnames"; import { useButton } from "@react-aria/button"; @@ -147,18 +147,22 @@ export const ButtonWithDropdown: FC<{ const [selectedUserId, setSelectedUserId] = useState(options[0].id); const onPress = useCallback(() => { - if (!selectedUserId) return; - onOptionSelect(selectedUserId); }, [onOptionSelect, selectedUserId]); const onSelectedOptionChange = useCallback( (event: ChangeEvent) => { - setSelectedUserId(event.target.value); + setSelectedUserId(event.target.options[event.target.selectedIndex].id); }, [setSelectedUserId], ); + useEffect(() => { + if (!options.find((o) => o.id === selectedUserId)) { + setSelectedUserId(options[0].id); + } + }, [options, selectedUserId, setSelectedUserId]); + return (
diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index b86d93521..5c399a0a2 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -109,6 +109,14 @@ const BreakoutRoomRow: FC = ({ [onUsersChanged, roomIndex, members], ); + const notAlreadyMembers = useMemo( + () => + parentRoomMembers.filter( + (rm) => !members.find((m) => rm.userId === m.userId), + ), + [parentRoomMembers, members], + ); + return (
@@ -132,17 +140,13 @@ const BreakoutRoomRow: FC = ({ onRemove={onRemoveUser} /> ))} - {parentRoomMembers.find( - (rm) => !members.find((m) => rm.userId === m.userId), - ) && ( + {notAlreadyMembers.length > 0 && ( !members.includes(m)) - .map((m) => ({ - label: m.name, - id: m.userId, - }))} + options={notAlreadyMembers.map((m) => ({ + label: m.name, + id: m.userId, + }))} onOptionSelect={onAddUser} /> )} @@ -252,7 +256,9 @@ export const BreakoutRoomModal: FC = ({ members={( r.users.map((u) => room?.getMember(u)) as RoomMember[] ).filter((m) => !!m)} - parentRoomMembers={roomMembers} + parentRoomMembers={roomMembers.filter( + (m) => client.getUserId() !== m.userId, + )} onRoomNameChanged={onRoomNameChanged} onUsersChanged={onUsersChanged} onRemove={onRemoveRoom} From 363568935ac05d83ccadbe8395bd37baac9e9910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 15:15:29 +0200 Subject: [PATCH 27/32] Don't forget to reset submitting state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index 5c399a0a2..bbecaf2c1 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -242,6 +242,7 @@ export const BreakoutRoomModal: FC = ({ await client.createBreakoutRooms(roomId, newBreakoutRooms); + setSubmitting(false); onDismiss(); }, [client, roomId, room, breakoutRooms, onDismiss]); From 9428ee6d5d35a0b8f9d9deb5a38a246b682f7b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 19 Oct 2023 15:17:22 +0200 Subject: [PATCH 28/32] Handle existing break-out rooms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/BreakoutRoomModal.tsx | 51 +++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index bbecaf2c1..6ab9d7890 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -17,7 +17,10 @@ limitations under the License. import { ChangeEvent, FC, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { MatrixClient, RoomMember } from "matrix-js-sdk"; -import { BreakoutRoom } from "matrix-js-sdk/src/@types/breakout"; +import { + BreakoutRoom, + BreakoutRoomWithSummary, +} from "matrix-js-sdk/src/@types/breakout"; import { Modal } from "../Modal"; import { Button, ButtonWithDropdown, RemoveButton } from "../button/Button"; @@ -37,6 +40,10 @@ interface NewBreakoutRoom { users: string[]; } +interface BreakoutRoomWithName extends BreakoutRoom { + roomName: string; +} + interface BreakoutRoomUserProps { userId: string; label: string; @@ -172,12 +179,38 @@ export const BreakoutRoomModal: FC = ({ const room = useMemo(() => client.getRoom(roomId), [client, roomId]); const roomMembers = useMemo(() => room?.getMembers() ?? [], [room]); + const existingBreakoutRooms: NewBreakoutRoom[] = useMemo( + () => + room + ?.getBreakoutRooms() + ?.reduce( + (rooms: BreakoutRoomWithName[], room: BreakoutRoomWithSummary) => { + const name = room.roomSummary.name; + if (name) { + rooms.push({ + roomId: room.roomId, + users: room.users, + roomName: name, + }); + } + return rooms; + }, + [], + ) ?? [], + [room], + ); const [submitting, setSubmitting] = useState(false); - const [breakoutRooms, setBreakoutRooms] = useState(() => [ - { roomName: t("Break-out room 1"), users: [] }, - { roomName: t("Break-out room 2"), users: [] }, - ]); + const [breakoutRooms, setBreakoutRooms] = useState< + (NewBreakoutRoom | BreakoutRoomWithName)[] + >(() => + existingBreakoutRooms.length > 0 + ? existingBreakoutRooms + : [ + { roomName: t("Break-out room 1"), users: [] }, + { roomName: t("Break-out room 2"), users: [] }, + ], + ); const onRoomNameChanged = useCallback( (index: number, newRoomName: string) => { @@ -220,12 +253,16 @@ export const BreakoutRoomModal: FC = ({ const content: ShareRoomKeyEventContent = {}; const newBreakoutRooms: BreakoutRoom[] = []; for (const breakoutRoom of breakoutRooms) { - const { roomId } = await createRoom(client, breakoutRoom.roomName, true); + const roomId = breakoutRoom.hasOwnProperty("roomId") + ? (breakoutRoom as BreakoutRoomWithName).roomId + : (await createRoom(client, breakoutRoom.roomName, true)).roomId; + + newBreakoutRooms.push({ roomId, users: breakoutRoom.users }); + const key = getLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId)); if (key) { content[roomId] = key; } - newBreakoutRooms.push({ roomId, users: breakoutRoom.users }); } if (roomMembers) { From 2d4339161adf2f73db569e6d089671370040c68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Jan 2024 21:32:26 +0100 Subject: [PATCH 29/32] Update `yarn.lock` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0d54a545f..182979fe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1999,10 +1999,10 @@ clsx "2.1.0" usehooks-ts "2.9.1" -"@matrix-org/matrix-sdk-crypto-wasm@^3.2.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-3.5.0.tgz#997d63ae12304142513fe93c5e0872ff10ca30b4" - integrity sha512-7as0jJTje+rFu9AF8LEO0tmhtHcou2YQnZOtpiP+lS5rDfIPv5CL8/eb45fzDnbQybt9Jm5zdjBdiLBEaUg2dQ== +"@matrix-org/matrix-sdk-crypto-wasm@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.1.0.tgz#9b470ed57cf82b0891f6e1cced1dba90c87ef668" + integrity sha512-/jCyvpDmgAybQWiRMmzflm4cneaRNVt8USqEV1RxoHBzlIE68LtLc9/HCfaPjkY7aYLHTbCrThR9GFXRWGyRxQ== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -7321,12 +7321,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#2cd63ca4b90eb2e4d22b45ae281a81c4514e757a": - version "30.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2cd63ca4b90eb2e4d22b45ae281a81c4514e757a" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#59b75c6eec32b69f35bc8599aa7e823b875721f6": + version "31.1.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/59b75c6eec32b69f35bc8599aa7e823b875721f6" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^3.2.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^4.1.0" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 6a9b9f94350127d4ef381c4ea8ef8a00d4137449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Jan 2024 21:35:28 +0100 Subject: [PATCH 30/32] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 45ad456ce..990bc32fd 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -64,7 +64,7 @@ "video": "Video" }, "Create rooms": "Create rooms", - "Creating rooms": "Creating rooms", + "Creating rooms...": "Creating rooms...", "disconnected_banner": "Connectivity to the server has been lost.", "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", "full_screen_view_h1": "<0>Oops, something's gone wrong.", From 45f02d22e2033dc4bfe477179180c4fdfa976d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Jan 2024 21:58:27 +0100 Subject: [PATCH 31/32] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 2 +- src/room/BreakoutRoomModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 990bc32fd..45ad456ce 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -64,7 +64,7 @@ "video": "Video" }, "Create rooms": "Create rooms", - "Creating rooms...": "Creating rooms...", + "Creating rooms": "Creating rooms", "disconnected_banner": "Connectivity to the server has been lost.", "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", "full_screen_view_h1": "<0>Oops, something's gone wrong.", diff --git a/src/room/BreakoutRoomModal.tsx b/src/room/BreakoutRoomModal.tsx index a7567ad3f..db204e979 100644 --- a/src/room/BreakoutRoomModal.tsx +++ b/src/room/BreakoutRoomModal.tsx @@ -310,7 +310,7 @@ export const BreakoutRoomModal: FC = ({ {t("Add break-out room")}
From b13bc631d188f7d74f090c30d5629fa301415157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 27 Jan 2024 22:04:14 +0100 Subject: [PATCH 32/32] Fix merge mess up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/button/Button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 0ce95c39c..f5a5230b9 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -136,6 +136,7 @@ export const Button = forwardRef( ); }, ); +Button.displayName = "Button"; export const ButtonWithDropdown: FC<{ label: string;