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

Commit

Permalink
Element Call video rooms (#9267)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
robintown committed Sep 16, 2022
1 parent db5716b commit cb735c9
Show file tree
Hide file tree
Showing 37 changed files with 1,694 additions and 1,379 deletions.
13 changes: 7 additions & 6 deletions res/css/views/rooms/_RoomPreviewCard.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,6 @@ limitations under the License.
color: $secondary-content;
}
}

/* XXX Remove this when video rooms leave beta */
.mx_BetaCard_betaPill {
margin-inline-start: auto;
align-self: start;
}
}

.mx_RoomPreviewCard_avatar {
Expand Down Expand Up @@ -104,6 +98,13 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}

/* XXX Remove this when video rooms leave beta */
.mx_BetaCard_betaPill {
position: absolute;
inset-block-start: $spacing-32;
inset-inline-end: $spacing-24;
}
}

h1.mx_RoomPreviewCard_name {
Expand Down
3 changes: 3 additions & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ export interface IConfigOptions {
voip?: {
obey_asserted_identity?: boolean; // MSC3086
};
element_call: {
url: string;
};

logout_redirect_url?: string;

Expand Down
17 changes: 6 additions & 11 deletions src/SdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const DEFAULTS: IConfigOptions = {
jitsi: {
preferred_domain: "meet.element.io",
},
element_call: {
url: "https://call.element.io",
},

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

public static put(cfg: IConfigOptions) {
const defaultKeys = Object.keys(DEFAULTS);
for (let i = 0; i < defaultKeys.length; ++i) {
if (cfg[defaultKeys[i]] === undefined) {
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
}
}
SdkConfig.setInstance(cfg);
public static put(cfg: Partial<IConfigOptions>) {
SdkConfig.setInstance({ ...DEFAULTS, ...cfg });
}

/**
Expand All @@ -97,9 +94,7 @@ export default class SdkConfig {
}

public static add(cfg: Partial<IConfigOptions>) {
const liveConfig = SdkConfig.get();
const newConfig = Object.assign({}, liveConfig, cfg);
SdkConfig.put(newConfig);
SdkConfig.put({ ...SdkConfig.get(), ...cfg });
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
import { LargeLoader } from './LargeLoader';
import { isVideoRoom } from '../../utils/video-rooms';

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

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

const myMembership = this.state.room.getMyMembership();
if (
this.state.room.isElementVideoRoom() &&
!(SettingsStore.getValue("feature_video_rooms") && myMembership === "join")
isVideoRoom(this.state.room)
&& !(SettingsStore.getValue("feature_video_rooms") && myMembership === "join")
) {
return <ErrorBoundary>
<div className="mx_MainSplit">
Expand Down
10 changes: 8 additions & 2 deletions src/components/structures/SpaceRoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ const SpaceLandingAddButton = ({ space }) => {
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
const canCreateSpace = shouldShowComponent(UIComponent.CreateSpaces);
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");

let contextMenu;
let contextMenu: JSX.Element | null = null;
if (menuDisplayed) {
const rect = handle.current.getBoundingClientRect();
contextMenu = <IconizedContextMenu
Expand Down Expand Up @@ -145,7 +146,12 @@ const SpaceLandingAddButton = ({ space }) => {
e.stopPropagation();
closeMenu();

if (await showCreateNewRoom(space, RoomType.ElementVideo)) {
if (
await showCreateNewRoom(
space,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
)
) {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
Expand Down
8 changes: 6 additions & 2 deletions src/components/views/context_menus/RoomContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,14 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
}

const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = videoRoomsEnabled && (
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
);

let inviteOption: JSX.Element;
if (room.canInvite(cli.getUserId()) && !isDm) {
if (room.canInvite(cli.getUserId()!) && !isDm) {
const onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Expand Down
21 changes: 12 additions & 9 deletions src/components/views/context_menus/SpaceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { BetaPill } from "../beta/BetaCard";
import SettingsStore from "../../../settings/SettingsStore";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { Action } from "../../../dispatcher/actions";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
Expand All @@ -48,9 +49,9 @@ interface IProps extends IContextMenuProps {

const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
const userId = cli.getUserId()!;

let inviteOption;
let inviteOption: JSX.Element | null = null;
if (space.getJoinRule() === "public" || space.canInvite(userId)) {
const onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
Expand All @@ -71,8 +72,8 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
);
}

let settingsOption;
let leaveOption;
let settingsOption: JSX.Element | null = null;
let leaveOption: JSX.Element | null = null;
if (shouldShowSpaceSettings(space)) {
const onSettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
Expand Down Expand Up @@ -110,7 +111,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
);
}

let devtoolsOption;
let devtoolsOption: JSX.Element | null = null;
if (SettingsStore.getValue("developerMode")) {
const onViewTimelineClick = (ev: ButtonEvent) => {
ev.preventDefault();
Expand All @@ -134,12 +135,15 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
);
}

const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");

const hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
const canAddVideoRooms = canAddRooms && SettingsStore.getValue("feature_video_rooms");
const canAddVideoRooms = canAddRooms && videoRoomsEnabled;
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);

let newRoomSection: JSX.Element;
let newRoomSection: JSX.Element | null = null;
if (canAddRooms || canAddSubSpaces) {
const onNewRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
Expand All @@ -154,7 +158,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
ev.preventDefault();
ev.stopPropagation();

showCreateNewRoom(space, RoomType.ElementVideo);
showCreateNewRoom(space, elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo);
onFinished();
};

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

export default SpaceContextMenu;

9 changes: 4 additions & 5 deletions src/components/views/context_menus/WidgetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,17 @@ const WidgetContextMenu: React.FC<IProps> = ({
/>;
}

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

const isLocalWidget = WidgetType.JITSI.matches(app.type);
let revokeButton;
if (!userWidget && !isLocalWidget && isAllowedWidget) {
const onRevokeClick = () => {
logger.info("Revoking permission for widget to load: " + app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[app.eventId] = false;
if (app.eventId !== undefined) current[app.eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch(err => {
logger.error(err);
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/ModalWidgetDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
}

public componentDidMount() {
const driver = new StopGapWidgetDriver([], this.widget, WidgetKind.Modal);
const driver = new StopGapWidgetDriver([], this.widget, WidgetKind.Modal, false);
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
this.setState({ messaging });
}
Expand Down
8 changes: 3 additions & 5 deletions src/components/views/elements/AppTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,8 @@ export default class AppTile extends React.Component<IProps, IState> {
if (!props.room) return true; // user widgets always have permissions

const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
if (currentlyAllowedWidgets[props.app.eventId] === undefined) {
return props.userId === props.creatorUserId;
}
return !!currentlyAllowedWidgets[props.app.eventId];
const allowed = props.app.eventId !== undefined && (currentlyAllowedWidgets[props.app.eventId] ?? false);
return allowed || props.userId === props.creatorUserId;
};

private onUserLeftRoom() {
Expand Down Expand Up @@ -442,7 +440,7 @@ export default class AppTile extends React.Component<IProps, IState> {
const roomId = this.props.room?.roomId;
logger.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.app.eventId] = true;
if (this.props.app.eventId !== undefined) current[this.props.app.eventId] = true;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
SettingsStore.setValue("allowedWidgets", roomId, level, current).then(() => {
this.setState({ hasPermissionToLoad: true });
Expand Down
57 changes: 20 additions & 37 deletions src/components/views/elements/PersistentApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";

import WidgetUtils from '../../../utils/WidgetUtils';
import AppTile from "./AppTile";
import { IApp } from '../../../stores/WidgetStore';
import WidgetStore from '../../../stores/WidgetStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";

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

constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
super(props, context);
this.room = context.getRoom(this.props.persistentRoomId);
this.room = context.getRoom(this.props.persistentRoomId)!;
}

private get app(): IApp | null {
// get the widget data
const appEvent = WidgetUtils.getRoomWidgets(this.room).find(ev =>
ev.getStateKey() === this.props.persistentWidgetId,
);

if (appEvent) {
return WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
this.room.roomId, appEvent.getId(),
);
} else {
return null;
}
}

public render(): JSX.Element {
const app = this.app;
if (app) {
return <AppTile
key={app.id}
app={app}
fullWidth={true}
room={this.room}
userId={this.context.credentials.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
miniMode={true}
showMenubar={false}
pointerEvents={this.props.pointerEvents}
movePersistedElement={this.props.movePersistedElement}
/>;
}
return null;
public render(): JSX.Element | null {
const app = WidgetStore.instance.get(this.props.persistentWidgetId, this.props.persistentRoomId);
if (!app) return null;

return <AppTile
key={app.id}
app={app}
fullWidth={true}
room={this.room}
userId={this.context.credentials.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
miniMode={true}
showMenubar={false}
pointerEvents={this.props.pointerEvents}
movePersistedElement={this.props.movePersistedElement}
/>;
}
}

6 changes: 5 additions & 1 deletion src/components/views/right_panel/RoomSummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,11 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useContext(RoomContext);
const e2eStatus = roomContext.e2eStatus;
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = videoRoomsEnabled && (
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
);

const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const header = <React.Fragment>
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/rooms/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
import { BetaPill } from "../beta/BetaCard";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";

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

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

const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isElementVideoRoom();
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
const viewLabs = () => defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
Expand Down
6 changes: 5 additions & 1 deletion src/components/views/rooms/RoomInfoLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import { useRoomState } from "../../../hooks/useRoomState";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { useRoomMemberCount, useMyRoomMembership } from "../../../hooks/useRoomMembers";
import AccessibleButton from "../elements/AccessibleButton";

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

const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom());

let iconClass: string;
let roomType: string;
if (room.isElementVideoRoom()) {
if (isVideoRoom) {
iconClass = "mx_RoomInfoLine_video";
roomType = _t("Video room");
} else if (joinRule === JoinRule.Public) {
Expand Down
Loading

0 comments on commit cb735c9

Please sign in to comment.