Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for break-out rooms #1615

Draft
wants to merge 35 commits into
base: livekit
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b5d710a
Add `arrayFastClone`
SimonBrandner Sep 26, 2023
1f72be4
Add `Remove` icon
SimonBrandner Sep 26, 2023
e1b1586
Add `RemoveButton`
SimonBrandner Sep 26, 2023
430fca6
Add basic `BreakoutRoomModal`
SimonBrandner Sep 26, 2023
7e3b05f
Add `BreakoutRoom` icon
SimonBrandner Sep 26, 2023
47f171c
Add `BreakoutRoomButton`
SimonBrandner Sep 26, 2023
0cda5fc
Present `BreakoutRoomButton` in UI
SimonBrandner Sep 26, 2023
fb5dcf8
Add new BreakoutRooms button
SimonBrandner Sep 29, 2023
65a2fab
Make sure we show loading when joining a new room
SimonBrandner Oct 1, 2023
83f2b80
Make sure to create a key for new breakout rooms
SimonBrandner Oct 1, 2023
e5838e1
Add `BreakoutRoomsOverlay`
SimonBrandner Oct 1, 2023
bbe70b3
Hack to make rooms visible in home screen
SimonBrandner Oct 1, 2023
af72a68
Add `ButtonWithDropdown`
SimonBrandner Oct 16, 2023
5938ea6
Add ability to add users
SimonBrandner Oct 16, 2023
c8183d4
Display users in overlay
SimonBrandner Oct 17, 2023
21052aa
Merge branch 'livekit' into break-out-rooms
SimonBrandner Oct 18, 2023
1cd0545
Update-js sdk
SimonBrandner Oct 19, 2023
17d6bfa
Merge remote-tracking branch 'upstream/livekit' into break-out-rooms
SimonBrandner Oct 19, 2023
4631a7d
Post-merge fix-up
SimonBrandner Oct 19, 2023
099b7ff
Post-merge fix-up
SimonBrandner Oct 19, 2023
90e2acd
Delint
SimonBrandner Oct 19, 2023
c66c1bc
Simplify break-out room handling
SimonBrandner Oct 19, 2023
a59ab95
Share break-out room keys
SimonBrandner Oct 19, 2023
95547f1
Delint
SimonBrandner Oct 19, 2023
0887065
i18n
SimonBrandner Oct 19, 2023
61d8927
Update js-sdk
SimonBrandner Oct 19, 2023
16bdcca
Delint
SimonBrandner Oct 19, 2023
b6b2911
Fix `ButtonWithDropdown` behaviour
SimonBrandner Oct 19, 2023
3635689
Don't forget to reset submitting state
SimonBrandner Oct 19, 2023
9428ee6
Handle existing break-out rooms
SimonBrandner Oct 19, 2023
82c1e63
Merge branch 'livekit' into break-out-rooms
SimonBrandner Jan 27, 2024
2d43391
Update `yarn.lock`
SimonBrandner Jan 27, 2024
6a9b9f9
i18n
SimonBrandner Jan 27, 2024
45f02d2
i18n
SimonBrandner Jan 27, 2024
b13bc63
Fix merge mess up
SimonBrandner Jan 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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#2cd63ca4b90eb2e4d22b45ae281a81c4514e757a",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#59b75c6eec32b69f35bc8599aa7e823b875721f6",
"matrix-widget-api": "^1.3.1",
"normalize.css": "^8.0.1",
"pako": "^2.0.4",
Expand Down
11 changes: 11 additions & 0 deletions public/locales/en-GB/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"sign_out": "Sign out",
"submit": "Submit"
},
"Add break-out room": "Add break-out room",
"Add user": "Add user",
"analytics_notice": "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</2> and our <5>Cookie Policy</5>.",
"app_selection_modal": {
"continue_in_browser": "Continue in browser",
Expand All @@ -23,6 +25,10 @@
"title": "Select app"
},
"application_opened_another_tab": "This application has been opened in another tab.",
"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",
"browser_media_e2ee_unsupported": "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117",
"browser_media_e2ee_unsupported_heading": "Incompatible Browser",
"call_ended_view": {
Expand Down Expand Up @@ -57,6 +63,8 @@
"username": "Username",
"video": "Video"
},
"Create rooms": "Create 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.</0>",
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
Expand Down Expand Up @@ -110,7 +118,9 @@
"register_auth_links": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
"register_confirm_password_label": "Confirm password",
"register_heading": "Create your account",
"Remove": "Remove",
"return_home_button": "Return to home screen",
"Room name": "Room name",
"room_auth_view_eula_caption": "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"room_auth_view_join_button": "Join call now",
"screenshare_button_label": "Share screen",
Expand All @@ -130,6 +140,7 @@
"show_connection_stats_label": "Show connection stats",
"speaker_device_selection_label": "Speaker"
},
"Settings": "Settings",
"star_rating_input_label_one": "{{count}} stars",
"star_rating_input_label_other": "{{count}} stars",
"start_new_call": "Start new call",
Expand Down
25 changes: 24 additions & 1 deletion src/ClientContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -44,6 +45,11 @@ import {
import { translatedError } from "./TranslatedError";
import { useEventTarget } from "./useEvents";
import { Config } from "./config/Config";
import {
SHARE_ROOM_KEY_EVENT_TYPE,
getRoomSharedKeyLocalStorageKey,
} from "./e2ee/sharedKeyManagement";
import { getLocalStorageItem, setLocalStorageItem } from "./useLocalStorage";

declare global {
interface Window {
Expand Down Expand Up @@ -302,6 +308,18 @@ export const ClientProvider: FC<Props> = ({ 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;
Expand All @@ -315,14 +333,19 @@ export const ClientProvider: FC<Props> = ({ 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]);
}, [initClientState, onSync, onToDevice]);

if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />;
Expand Down
17 changes: 17 additions & 0 deletions src/button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,23 @@ limitations under the License.
cursor: pointer;
}

.removeButton {
width: 24px;
height: 24px;

margin: 12px;

background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: 100%;
}

.buttonWithDropdown {
display: flex;
flex-direction: row;

gap: 8px;
}

@media (hover: hover) {
.toolbarButton:hover,
.toolbarButtonSecondary:hover {
Expand Down
91 changes: 88 additions & 3 deletions src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ 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 } 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";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@vector-im/compound-web";
import { ChangeEvent } from "react";
import MicOnSolidIcon from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg?react";
import MicOffSolidIcon from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg?react";
import VideoCallSolidIcon from "@vector-im/compound-design-tokens/icons/video-call-solid.svg?react";
Expand All @@ -30,6 +31,9 @@ import SettingsSolidIcon from "@vector-im/compound-design-tokens/icons/settings-
import ChevronDownIcon from "@vector-im/compound-design-tokens/icons/chevron-down.svg?react";

import styles from "./Button.module.css";
import RemoveIcon from "../icons/Remove.svg?react";
import AddBreakoutRoomIcon from "../icons/AddBreakoutRoom.svg?react";
import BreakoutRoomsIcon from "../icons/BreakoutRooms.svg?react";

export type ButtonVariant =
| "default"
Expand Down Expand Up @@ -132,9 +136,48 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
);
},
);

Button.displayName = "Button";

export const ButtonWithDropdown: FC<{
label: string;
options: { label: string; id: string }[];
onOptionSelect: (id: string) => void;
}> = ({ label, options, onOptionSelect, ...rest }) => {
const [selectedUserId, setSelectedUserId] = useState<string>(options[0].id);

const onPress = useCallback(() => {
onOptionSelect(selectedUserId);
}, [onOptionSelect, selectedUserId]);

const onSelectedOptionChange = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
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 (
<div className={styles.buttonWithDropdown}>
<Tooltip label={label}>
<Button onPress={onPress} {...rest}>
{label}
</Button>
</Tooltip>
<select onChange={onSelectedOptionChange}>
{options.map((o) => (
<option id={o.id}> {o.label}</option>
))}
</select>
</div>
);
};

export const MicButton: FC<{
muted: boolean;
// TODO: add all props for <Button>
Expand Down Expand Up @@ -225,7 +268,49 @@ export const SettingsButton: FC<{
return (
<Tooltip label={t("common.settings")}>
<Button variant="toolbar" {...rest}>
<SettingsSolidIcon aria-label={t("common.settings")} />
<SettingsSolidIcon aria-label={t("Settings")} />
</Button>
</Tooltip>
);
};

export const AddBreakoutRoomButton: FC<{
// TODO: add all props for <Button>
[index: string]: unknown;
}> = ({ ...rest }) => {
const { t } = useTranslation();

return (
<Tooltip label={t("Break-out room")}>
<Button variant="toolbar" {...rest}>
<AddBreakoutRoomIcon aria-label={t("Break-out room")} />
</Button>
</Tooltip>
);
};

export const BreakoutRoomsButton: FC<{
// TODO: add all props for <Button>
[index: string]: unknown;
}> = ({ ...rest }) => {
const { t } = useTranslation();

return (
<Tooltip label={t("Break-out rooms")}>
<Button variant="toolbar" {...rest}>
<BreakoutRoomsIcon aria-label={t("Break-out rooms")} />
</Button>
</Tooltip>
);
};

export const RemoveButton: FC<Omit<Props, "variant">> = ({ ...rest }) => {
const { t } = useTranslation();

return (
<Tooltip label={t("Remove")}>
<Button className={styles.removeButton} variant="icon" {...rest}>
<RemoveIcon aria-label={t("Remove")} />
</Button>
</Tooltip>
);
Expand Down
5 changes: 4 additions & 1 deletion src/e2ee/sharedKeyManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import { useClient } from "../ClientContext";
import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
import { widget } from "../widget";

export type ShareRoomKeyEventContent = Record<string, string>;
export const SHARE_ROOM_KEY_EVENT_TYPE = "io.element.share_room_key";

export function saveKeyForRoom(roomId: string, password: string): void {
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
}

const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`;

const useInternalRoomSharedKey = (roomId: string): string | null => {
Expand Down
3 changes: 3 additions & 0 deletions src/icons/AddBreakoutRoom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/icons/BreakoutRooms.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/icons/Remove.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions src/room/BreakoutRoomModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2023 Šimon Brandner <simon.bra.ag@gmail.com>

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;
}

.breakoutRoom {
display: flex;
flex-direction: column;
width: 100%;
}

.breakoutRoomNameFieldRow {
display: flex;
flex-direction: row;
align-items: center;

margin-top: 10px;
margin-bottom: 10px;

width: 100%;
}

.breakoutRoomNameField {
margin-right: 0;
}

.breakoutRoomUser {
display: flex;
flex-direction: row;
}

.breakoutRoomUser button {
margin: 0;
margin-left: 6px;
}

.breakoutRoomsButtons {
display: flex;
flex-direction: row;
justify-content: space-between;
}
Loading