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 37056514fbc040aaf1bff2539da770a1c8ba72a2.

* 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
This commit is contained in:
Robin 2022-09-16 11:12:27 -04:00 committed by GitHub
parent db5716b776
commit cb735c9439
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1699 additions and 1384 deletions

View file

@ -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();

View file

@ -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";
@ -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();
@ -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();
@ -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();
@ -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();
@ -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();
};
@ -266,4 +270,3 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
};
export default SpaceContextMenu;

View file

@ -146,10 +146,9 @@ 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;
@ -157,7 +156,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
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);

View file

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

View file

@ -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() {
@ -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 });

View file

@ -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 {
@ -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,
);
public render(): JSX.Element | null {
const app = WidgetStore.instance.get(this.props.persistentWidgetId, this.props.persistentRoomId);
if (!app) return null;
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;
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}
/>;
}
}

View file

@ -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>

View file

@ -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;
@ -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,

View file

@ -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";
@ -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) {

View file

@ -32,6 +32,7 @@ import { _t, _td } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import PosthogTrackers from "../../../PosthogTrackers";
import SettingsStore from "../../../settings/SettingsStore";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { UIComponent } from "../../../settings/UIFeature";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
@ -200,8 +201,10 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
});
const showCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
let contextMenuContent: JSX.Element;
let contextMenuContent: JSX.Element | null = null;
if (menuDisplayed && activeSpace) {
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
@ -239,7 +242,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
{ SettingsStore.getValue("feature_video_rooms") && (
{ videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
@ -247,7 +250,10 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewRoom(activeSpace, RoomType.ElementVideo);
showCreateNewRoom(
activeSpace,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -287,7 +293,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
}}
/>
{ SettingsStore.getValue("feature_video_rooms") && (
{ videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
@ -297,7 +303,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
closeMenu();
defaultDispatcher.dispatch({
action: "view_create_room",
type: RoomType.ElementVideo,
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
}}
>
@ -319,7 +325,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
</IconizedContextMenuOptionList>;
}
let contextMenu: JSX.Element;
let contextMenu: JSX.Element | null = null;
if (menuDisplayed) {
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
{ contextMenuContent }

View file

@ -127,6 +127,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
return SpaceStore.instance.allRoomsInHome;
});
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const pendingActions = usePendingActions();
const canShowMainMenu = activeSpace || spaceKey === MetaSpace.Home;
@ -211,7 +212,10 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace, RoomType.ElementVideo);
showCreateNewRoom(
activeSpace,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
);
closePlusMenu();
}}
>
@ -310,7 +314,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
e.stopPropagation();
defaultDispatcher.dispatch({
action: "view_create_room",
type: RoomType.ElementVideo,
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
closePlusMenu();
}}

View file

@ -51,6 +51,8 @@ interface IProps {
const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom());
const myMembership = useMyRoomMembership(room);
useDispatcher(defaultDispatcher, payload => {
if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) {
@ -69,7 +71,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
initialTabId: UserTab.Labs,
});
let inviterSection: JSX.Element;
let inviterSection: JSX.Element | null = null;
let joinButtons: JSX.Element;
if (myMembership === "join") {
joinButtons = (
@ -86,10 +88,11 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
</AccessibleButton>
);
} else if (myMembership === "invite") {
const inviteSender = room.getMember(cli.getUserId())?.events.member?.getSender();
const inviter = inviteSender && room.getMember(inviteSender);
const inviteSender = room.getMember(cli.getUserId()!)?.events.member?.getSender();
if (inviteSender) {
const inviter = room.getMember(inviteSender);
inviterSection = <div className="mx_RoomPreviewCard_inviter">
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
<div>
@ -102,10 +105,6 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
{ inviteSender }
</div> : null }
</div>
{ room.isElementVideoRoom()
? <BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
: null
}
</div>;
}
@ -152,10 +151,11 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
}
let avatarRow: JSX.Element;
if (room.isElementVideoRoom()) {
if (isVideoRoom) {
avatarRow = <>
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
<div className="mx_RoomPreviewCard_video" />
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
</>;
} else if (room.isSpaceRoom()) {
avatarRow = <RoomAvatar room={room} height={80} width={80} viewAvatarOnClick />;
@ -163,12 +163,12 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
avatarRow = <RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />;
}
let notice: string;
let notice: string | null = null;
if (cannotJoin) {
notice = _t("To view %(roomName)s, you need an invite", {
roomName: room.name,
});
} else if (room.isElementVideoRoom() && !videoRoomsEnabled) {
} else if (isVideoRoom && !videoRoomsEnabled) {
notice = myMembership === "join"
? _t("To view, please enable video rooms in Labs first")
: _t("To join, please enable video rooms in Labs first");