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

Commit cb735c9

Browse files
authored
Element Call video rooms (#9267)
* Add an element_call_url config option * Add a labs flag for Element Call video rooms * Add Element Call as another video rooms backend * Consolidate event power level defaults * Remember to clean up participantsExpirationTimer * Fix a code smell * Test the clean method * Fix some strict mode errors * Test that clean still works when there are no state events * Test auto-approval of Element Call widget capabilities * Deduplicate some code to placate SonarCloud * Fix more strict mode errors * Test that calls disconnect when leaving the room * Test the get methods of JitsiCall and ElementCall more * Test Call.ts even more * Test creation of Element video rooms * Test that createRoom works for non-video-rooms * Test Call's get method rather than the methods of derived classes * Ensure that the clean method is able to preserve devices * Remove duplicate clean method * Fix lints * Fix some strict mode errors in RoomPreviewCard * Test RoomPreviewCard changes * Quick and dirty hotfix for the community testing session * Revert "Quick and dirty hotfix for the community testing session" This reverts commit 3705651. * Fix the event schema for org.matrix.msc3401.call.member devices * Remove org.matrix.call_duplicate_session from Element Call capabilities It's no longer used by Element Call when running as a widget. * Replace element_call_url with a map * Make PiPs work for virtual widgets * Auto-approve room timeline capability Because Element Call uses this now * Create a reusable isVideoRoom util
1 parent db5716b commit cb735c9

37 files changed

+1694
-1379
lines changed

res/css/views/rooms/_RoomPreviewCard.pcss

+7-6
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ limitations under the License.
6464
color: $secondary-content;
6565
}
6666
}
67-
68-
/* XXX Remove this when video rooms leave beta */
69-
.mx_BetaCard_betaPill {
70-
margin-inline-start: auto;
71-
align-self: start;
72-
}
7367
}
7468

7569
.mx_RoomPreviewCard_avatar {
@@ -104,6 +98,13 @@ limitations under the License.
10498
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
10599
}
106100
}
101+
102+
/* XXX Remove this when video rooms leave beta */
103+
.mx_BetaCard_betaPill {
104+
position: absolute;
105+
inset-block-start: $spacing-32;
106+
inset-inline-end: $spacing-24;
107+
}
107108
}
108109

109110
h1.mx_RoomPreviewCard_name {

src/IConfigOptions.ts

+3
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ export interface IConfigOptions {
116116
voip?: {
117117
obey_asserted_identity?: boolean; // MSC3086
118118
};
119+
element_call: {
120+
url: string;
121+
};
119122

120123
logout_redirect_url?: string;
121124

src/SdkConfig.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export const DEFAULTS: IConfigOptions = {
3030
jitsi: {
3131
preferred_domain: "meet.element.io",
3232
},
33+
element_call: {
34+
url: "https://call.element.io",
35+
},
3336

3437
// @ts-ignore - we deliberately use the camelCase version here so we trigger
3538
// the fallback behaviour. If we used the snake_case version then we'd break
@@ -79,14 +82,8 @@ export default class SdkConfig {
7982
return val === undefined ? undefined : null;
8083
}
8184

82-
public static put(cfg: IConfigOptions) {
83-
const defaultKeys = Object.keys(DEFAULTS);
84-
for (let i = 0; i < defaultKeys.length; ++i) {
85-
if (cfg[defaultKeys[i]] === undefined) {
86-
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
87-
}
88-
}
89-
SdkConfig.setInstance(cfg);
85+
public static put(cfg: Partial<IConfigOptions>) {
86+
SdkConfig.setInstance({ ...DEFAULTS, ...cfg });
9087
}
9188

9289
/**
@@ -97,9 +94,7 @@ export default class SdkConfig {
9794
}
9895

9996
public static add(cfg: Partial<IConfigOptions>) {
100-
const liveConfig = SdkConfig.get();
101-
const newConfig = Object.assign({}, liveConfig, cfg);
102-
SdkConfig.put(newConfig);
97+
SdkConfig.put({ ...SdkConfig.get(), ...cfg });
10398
}
10499
}
105100

src/components/structures/RoomView.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
119119
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
120120
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
121121
import { LargeLoader } from './LargeLoader';
122+
import { isVideoRoom } from '../../utils/video-rooms';
122123

123124
const DEBUG = false;
124125
let debuglog = function(msg: string) {};
@@ -514,7 +515,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
514515
};
515516

516517
private getMainSplitContentType = (room: Room) => {
517-
if (SettingsStore.getValue("feature_video_rooms") && room.isElementVideoRoom()) {
518+
if (SettingsStore.getValue("feature_video_rooms") && isVideoRoom(room)) {
518519
return MainSplitContentType.Video;
519520
}
520521
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
@@ -2015,8 +2016,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20152016

20162017
const myMembership = this.state.room.getMyMembership();
20172018
if (
2018-
this.state.room.isElementVideoRoom() &&
2019-
!(SettingsStore.getValue("feature_video_rooms") && myMembership === "join")
2019+
isVideoRoom(this.state.room)
2020+
&& !(SettingsStore.getValue("feature_video_rooms") && myMembership === "join")
20202021
) {
20212022
return <ErrorBoundary>
20222023
<div className="mx_MainSplit">

src/components/structures/SpaceRoomView.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ const SpaceLandingAddButton = ({ space }) => {
108108
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
109109
const canCreateSpace = shouldShowComponent(UIComponent.CreateSpaces);
110110
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
111+
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
111112

112-
let contextMenu;
113+
let contextMenu: JSX.Element | null = null;
113114
if (menuDisplayed) {
114115
const rect = handle.current.getBoundingClientRect();
115116
contextMenu = <IconizedContextMenu
@@ -145,7 +146,12 @@ const SpaceLandingAddButton = ({ space }) => {
145146
e.stopPropagation();
146147
closeMenu();
147148

148-
if (await showCreateNewRoom(space, RoomType.ElementVideo)) {
149+
if (
150+
await showCreateNewRoom(
151+
space,
152+
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
153+
)
154+
) {
149155
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
150156
}
151157
}}

src/components/views/context_menus/RoomContextMenu.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,14 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
105105
}
106106

107107
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
108-
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
108+
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
109+
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
110+
const isVideoRoom = videoRoomsEnabled && (
111+
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
112+
);
109113

110114
let inviteOption: JSX.Element;
111-
if (room.canInvite(cli.getUserId()) && !isDm) {
115+
if (room.canInvite(cli.getUserId()!) && !isDm) {
112116
const onInviteClick = (ev: ButtonEvent) => {
113117
ev.preventDefault();
114118
ev.stopPropagation();

src/components/views/context_menus/SpaceContextMenu.tsx

+12-9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { ButtonEvent } from "../elements/AccessibleButton";
3535
import defaultDispatcher from "../../../dispatcher/dispatcher";
3636
import { BetaPill } from "../beta/BetaCard";
3737
import SettingsStore from "../../../settings/SettingsStore";
38+
import { useFeatureEnabled } from "../../../hooks/useSettings";
3839
import { Action } from "../../../dispatcher/actions";
3940
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
4041
import { UIComponent } from "../../../settings/UIFeature";
@@ -48,9 +49,9 @@ interface IProps extends IContextMenuProps {
4849

4950
const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) => {
5051
const cli = useContext(MatrixClientContext);
51-
const userId = cli.getUserId();
52+
const userId = cli.getUserId()!;
5253

53-
let inviteOption;
54+
let inviteOption: JSX.Element | null = null;
5455
if (space.getJoinRule() === "public" || space.canInvite(userId)) {
5556
const onInviteClick = (ev: ButtonEvent) => {
5657
ev.preventDefault();
@@ -71,8 +72,8 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
7172
);
7273
}
7374

74-
let settingsOption;
75-
let leaveOption;
75+
let settingsOption: JSX.Element | null = null;
76+
let leaveOption: JSX.Element | null = null;
7677
if (shouldShowSpaceSettings(space)) {
7778
const onSettingsClick = (ev: ButtonEvent) => {
7879
ev.preventDefault();
@@ -110,7 +111,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
110111
);
111112
}
112113

113-
let devtoolsOption;
114+
let devtoolsOption: JSX.Element | null = null;
114115
if (SettingsStore.getValue("developerMode")) {
115116
const onViewTimelineClick = (ev: ButtonEvent) => {
116117
ev.preventDefault();
@@ -134,12 +135,15 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
134135
);
135136
}
136137

138+
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
139+
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
140+
137141
const hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
138142
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
139-
const canAddVideoRooms = canAddRooms && SettingsStore.getValue("feature_video_rooms");
143+
const canAddVideoRooms = canAddRooms && videoRoomsEnabled;
140144
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
141145

142-
let newRoomSection: JSX.Element;
146+
let newRoomSection: JSX.Element | null = null;
143147
if (canAddRooms || canAddSubSpaces) {
144148
const onNewRoomClick = (ev: ButtonEvent) => {
145149
ev.preventDefault();
@@ -154,7 +158,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
154158
ev.preventDefault();
155159
ev.stopPropagation();
156160

157-
showCreateNewRoom(space, RoomType.ElementVideo);
161+
showCreateNewRoom(space, elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo);
158162
onFinished();
159163
};
160164

@@ -266,4 +270,3 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
266270
};
267271

268272
export default SpaceContextMenu;
269-

src/components/views/context_menus/WidgetContextMenu.tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,17 @@ const WidgetContextMenu: React.FC<IProps> = ({
146146
/>;
147147
}
148148

149-
let isAllowedWidget = SettingsStore.getValue("allowedWidgets", roomId)[app.eventId];
150-
if (isAllowedWidget === undefined) {
151-
isAllowedWidget = app.creatorUserId === cli.getUserId();
152-
}
149+
const isAllowedWidget =
150+
(app.eventId !== undefined && (SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false))
151+
|| app.creatorUserId === cli.getUserId();
153152

154153
const isLocalWidget = WidgetType.JITSI.matches(app.type);
155154
let revokeButton;
156155
if (!userWidget && !isLocalWidget && isAllowedWidget) {
157156
const onRevokeClick = () => {
158157
logger.info("Revoking permission for widget to load: " + app.eventId);
159158
const current = SettingsStore.getValue("allowedWidgets", roomId);
160-
current[app.eventId] = false;
159+
if (app.eventId !== undefined) current[app.eventId] = false;
161160
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
162161
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch(err => {
163162
logger.error(err);

src/components/views/dialogs/ModalWidgetDialog.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
7878
}
7979

8080
public componentDidMount() {
81-
const driver = new StopGapWidgetDriver([], this.widget, WidgetKind.Modal);
81+
const driver = new StopGapWidgetDriver([], this.widget, WidgetKind.Modal, false);
8282
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
8383
this.setState({ messaging });
8484
}

src/components/views/elements/AppTile.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,8 @@ export default class AppTile extends React.Component<IProps, IState> {
165165
if (!props.room) return true; // user widgets always have permissions
166166

167167
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
168-
if (currentlyAllowedWidgets[props.app.eventId] === undefined) {
169-
return props.userId === props.creatorUserId;
170-
}
171-
return !!currentlyAllowedWidgets[props.app.eventId];
168+
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);
169+
return allowed || props.userId === props.creatorUserId;
172170
};
173171

174172
private onUserLeftRoom() {
@@ -442,7 +440,7 @@ export default class AppTile extends React.Component<IProps, IState> {
442440
const roomId = this.props.room?.roomId;
443441
logger.info("Granting permission for widget to load: " + this.props.app.eventId);
444442
const current = SettingsStore.getValue("allowedWidgets", roomId);
445-
current[this.props.app.eventId] = true;
443+
if (this.props.app.eventId !== undefined) current[this.props.app.eventId] = true;
446444
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
447445
SettingsStore.setValue("allowedWidgets", roomId, level, current).then(() => {
448446
this.setState({ hasPermissionToLoad: true });

src/components/views/elements/PersistentApp.tsx

+20-37
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
2020

2121
import WidgetUtils from '../../../utils/WidgetUtils';
2222
import AppTile from "./AppTile";
23-
import { IApp } from '../../../stores/WidgetStore';
23+
import WidgetStore from '../../../stores/WidgetStore';
2424
import MatrixClientContext from "../../../contexts/MatrixClientContext";
2525

2626
interface IProps {
@@ -37,44 +37,27 @@ export default class PersistentApp extends React.Component<IProps> {
3737

3838
constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
3939
super(props, context);
40-
this.room = context.getRoom(this.props.persistentRoomId);
40+
this.room = context.getRoom(this.props.persistentRoomId)!;
4141
}
4242

43-
private get app(): IApp | null {
44-
// get the widget data
45-
const appEvent = WidgetUtils.getRoomWidgets(this.room).find(ev =>
46-
ev.getStateKey() === this.props.persistentWidgetId,
47-
);
48-
49-
if (appEvent) {
50-
return WidgetUtils.makeAppConfig(
51-
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
52-
this.room.roomId, appEvent.getId(),
53-
);
54-
} else {
55-
return null;
56-
}
57-
}
58-
59-
public render(): JSX.Element {
60-
const app = this.app;
61-
if (app) {
62-
return <AppTile
63-
key={app.id}
64-
app={app}
65-
fullWidth={true}
66-
room={this.room}
67-
userId={this.context.credentials.userId}
68-
creatorUserId={app.creatorUserId}
69-
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
70-
waitForIframeLoad={app.waitForIframeLoad}
71-
miniMode={true}
72-
showMenubar={false}
73-
pointerEvents={this.props.pointerEvents}
74-
movePersistedElement={this.props.movePersistedElement}
75-
/>;
76-
}
77-
return null;
43+
public render(): JSX.Element | null {
44+
const app = WidgetStore.instance.get(this.props.persistentWidgetId, this.props.persistentRoomId);
45+
if (!app) return null;
46+
47+
return <AppTile
48+
key={app.id}
49+
app={app}
50+
fullWidth={true}
51+
room={this.room}
52+
userId={this.context.credentials.userId}
53+
creatorUserId={app.creatorUserId}
54+
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
55+
waitForIframeLoad={app.waitForIframeLoad}
56+
miniMode={true}
57+
showMenubar={false}
58+
pointerEvents={this.props.pointerEvents}
59+
movePersistedElement={this.props.movePersistedElement}
60+
/>;
7861
}
7962
}
8063

src/components/views/right_panel/RoomSummaryCard.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,11 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
262262
const isRoomEncrypted = useIsEncrypted(cli, room);
263263
const roomContext = useContext(RoomContext);
264264
const e2eStatus = roomContext.e2eStatus;
265-
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
265+
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
266+
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
267+
const isVideoRoom = videoRoomsEnabled && (
268+
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
269+
);
266270

267271
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
268272
const header = <React.Fragment>

src/components/views/rooms/RoomHeader.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
4747
import { BetaPill } from "../beta/BetaCard";
4848
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
4949
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
50+
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
5051

5152
export interface ISearchInfo {
5253
searchTerm: string;
@@ -312,7 +313,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
312313

313314
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
314315

315-
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isElementVideoRoom();
316+
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
316317
const viewLabs = () => defaultDispatcher.dispatch({
317318
action: Action.ViewUserSettings,
318319
initialTabId: UserTab.Labs,

src/components/views/rooms/RoomInfoLine.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2323
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
2424
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
2525
import { useRoomState } from "../../../hooks/useRoomState";
26+
import { useFeatureEnabled } from "../../../hooks/useSettings";
2627
import { useRoomMemberCount, useMyRoomMembership } from "../../../hooks/useRoomMembers";
2728
import AccessibleButton from "../elements/AccessibleButton";
2829

@@ -44,9 +45,12 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
4445
const membership = useMyRoomMembership(room);
4546
const memberCount = useRoomMemberCount(room);
4647

48+
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
49+
const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom());
50+
4751
let iconClass: string;
4852
let roomType: string;
49-
if (room.isElementVideoRoom()) {
53+
if (isVideoRoom) {
5054
iconClass = "mx_RoomInfoLine_video";
5155
roomType = _t("Video room");
5256
} else if (joinRule === JoinRule.Public) {

0 commit comments

Comments
 (0)