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

Lines changed: 7 additions & 6 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 11 deletions
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

Lines changed: 4 additions & 3 deletions
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

Lines changed: 8 additions & 2 deletions
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

Lines changed: 6 additions & 2 deletions
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

Lines changed: 12 additions & 9 deletions
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

Lines changed: 4 additions & 5 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 5 deletions
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 });

0 commit comments

Comments
 (0)