Iterate video room designs in labs (#8499)

* Remove blank header from video room view frame

* Add video room option to space context menu

* Remove duplicate tooltips from face piles

* Factor RoomInfoLine out of SpaceRoomView

* Factor RoomPreviewCard out of SpaceRoomView

* Adapt RoomPreviewCard for video rooms

* "New video room" → "Video room"

* Add comment about unused cases in RoomPreviewCard

* Make widgets in video rooms mutable again to de-risk future upgrades

* Ensure that the video channel exists when mounting VideoRoomView
This commit is contained in:
Robin 2022-05-04 17:02:06 -04:00 committed by GitHub
parent cda31136b9
commit 658ff4dfe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 617 additions and 434 deletions

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
@ -136,6 +136,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
const hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
const canAddVideoRooms = canAddRooms && SettingsStore.getValue("feature_video_rooms");
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
let newRoomSection: JSX.Element;
@ -149,6 +150,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
onFinished();
};
const onNewVideoRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showCreateNewRoom(space, RoomType.ElementVideo);
onFinished();
};
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -169,6 +178,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
onClick={onNewRoomClick}
/>
}
{ canAddVideoRooms &&
<IconizedContextMenuOption
data-test-id='new-video-room-option'
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Video room")}
onClick={onNewVideoRoomClick}
/>
}
{ canAddSubSpaces &&
<IconizedContextMenuOption
data-test-id='new-subspace-option'

View file

@ -31,10 +31,16 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
const FacePile: FC<IProps> = ({ members, faceSize, overflow, tooltip, children, ...props }) => {
const faces = members.map(
tooltip ?
m => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} /> :
m => <TooltipTarget key={m.userId} label={m.name}>
<MemberAvatar member={m} width={faceSize} height={faceSize} viewUserOnClick={!props.onClick} />
tooltip
? m => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} hideTitle />
: m => <TooltipTarget key={m.userId} label={m.name}>
<MemberAvatar
member={m}
width={faceSize}
height={faceSize}
viewUserOnClick={!props.onClick}
hideTitle
/>
</TooltipTarget>,
);

View file

@ -0,0 +1,86 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
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.
*/
import React, { FC } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from "../../../languageHandler";
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 { useRoomMemberCount, useMyRoomMembership } from "../../../hooks/useRoomMembers";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
room: Room;
}
const RoomInfoLine: FC<IProps> = ({ room }) => {
// summary will begin as undefined whilst loading and go null if it fails to load or we are not invited.
const summary = useAsyncMemo(async () => {
if (room.getMyMembership() !== "invite") return null;
try {
return room.client.getRoomSummary(room.roomId);
} catch (e) {
return null;
}
}, [room]);
const joinRule = useRoomState(room, state => state.getJoinRule());
const membership = useMyRoomMembership(room);
const memberCount = useRoomMemberCount(room);
let iconClass: string;
let roomType: string;
if (room.isElementVideoRoom()) {
iconClass = "mx_RoomInfoLine_video";
roomType = _t("Video room");
} else if (joinRule === JoinRule.Public) {
iconClass = "mx_RoomInfoLine_public";
roomType = room.isSpaceRoom() ? _t("Public space") : _t("Public room");
} else {
iconClass = "mx_RoomInfoLine_private";
roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
}
let members: JSX.Element;
if (membership === "invite" && summary) {
// Don't trust local state and instead use the summary API
members = <span className="mx_RoomInfoLine_members">
{ _t("%(count)s members", { count: summary.num_joined_members }) }
</span>;
} else if (memberCount && summary !== undefined) { // summary is not still loading
const viewMembers = () => RightPanelStore.instance.setCard({
phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
});
members = <AccessibleButton
kind="link"
className="mx_RoomInfoLine_members"
onClick={viewMembers}
>
{ _t("%(count)s members", { count: memberCount }) }
</AccessibleButton>;
}
return <div className={`mx_RoomInfoLine ${iconClass}`}>
{ roomType }
{ members }
</div>;
};
export default RoomInfoLine;

View file

@ -239,7 +239,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
: _t("You do not have permissions to create new rooms in this space")}
/>
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
label={_t("New video room")}
label={_t("Video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
@ -283,7 +283,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
}}
/>
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
label={_t("New video room")}
label={_t("Video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();

View file

@ -222,7 +222,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
/>
{ videoRoomsEnabled && <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("New video room")}
label={_t("Video room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -313,7 +313,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
}}
/>
{ videoRoomsEnabled && <IconizedContextMenuOption
label={_t("New video room")}
label={_t("Video room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();

View file

@ -0,0 +1,202 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
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.
*/
import React, { FC, useContext, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from "../../../languageHandler";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserTab";
import { EffectiveMembership, getEffectiveMembership } from "../../../utils/membership";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useDispatcher } from "../../../hooks/useDispatcher";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { useRoomState } from "../../../hooks/useRoomState";
import { useMyRoomMembership } from "../../../hooks/useRoomMembers";
import AccessibleButton from "../elements/AccessibleButton";
import InlineSpinner from "../elements/InlineSpinner";
import RoomName from "../elements/RoomName";
import RoomTopic from "../elements/RoomTopic";
import RoomFacePile from "../elements/RoomFacePile";
import RoomAvatar from "../avatars/RoomAvatar";
import MemberAvatar from "../avatars/MemberAvatar";
import RoomInfoLine from "./RoomInfoLine";
interface IProps {
room: Room;
onJoinButtonClicked: () => void;
onRejectButtonClicked: () => void;
}
// XXX This component is currently only used for spaces and video rooms, though
// surely we should expand its use to all rooms for consistency? This already
// handles the text room case, though we would need to add support for ignoring
// and viewing invite reasons to achieve parity with the default invite screen.
const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const myMembership = useMyRoomMembership(room);
useDispatcher(defaultDispatcher, payload => {
if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) {
setBusy(false); // stop the spinner, join failed
}
});
const [busy, setBusy] = useState(false);
const joinRule = useRoomState(room, state => state.getJoinRule());
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
&& joinRule !== JoinRule.Public;
const viewLabs = () => defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
let inviterSection: JSX.Element;
let joinButtons: JSX.Element;
if (myMembership === "join") {
joinButtons = (
<AccessibleButton
kind="danger_outline"
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
}}
>
{ _t("Leave") }
</AccessibleButton>
);
} else if (myMembership === "invite") {
const inviteSender = room.getMember(cli.getUserId())?.events.member?.getSender();
const inviter = inviteSender && room.getMember(inviteSender);
if (inviteSender) {
inviterSection = <div className="mx_RoomPreviewCard_inviter">
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
<div>
<div className="mx_RoomPreviewCard_inviter_name">
{ _t("<inviter/> invites you", {}, {
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
}) }
</div>
{ inviter ? <div className="mx_RoomPreviewCard_inviter_mxid">
{ inviteSender }
</div> : null }
</div>
</div>;
}
joinButtons = <>
<AccessibleButton
kind="secondary"
onClick={() => {
setBusy(true);
onRejectButtonClicked();
}}
>
{ _t("Reject") }
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
>
{ _t("Accept") }
</AccessibleButton>
</>;
} else {
joinButtons = (
<AccessibleButton
kind="primary"
onClick={() => {
onJoinButtonClicked();
if (!cli.isGuest()) {
// user will be shown a modal that won't fire a room join error
setBusy(true);
}
}}
disabled={cannotJoin}
>
{ _t("Join") }
</AccessibleButton>
);
}
if (busy) {
joinButtons = <InlineSpinner />;
}
let avatarRow: JSX.Element;
if (room.isElementVideoRoom()) {
avatarRow = <>
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
<div className="mx_RoomPreviewCard_video" />
</>;
} else if (room.isSpaceRoom()) {
avatarRow = <RoomAvatar room={room} height={80} width={80} viewAvatarOnClick />;
} else {
avatarRow = <RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />;
}
let notice: string;
if (cannotJoin) {
notice = _t("To view %(roomName)s, you need an invite", {
roomName: room.name,
});
} else if (room.isElementVideoRoom() && !videoRoomsEnabled) {
notice = myMembership === "join"
? _t("To view, please enable video rooms in Labs first")
: _t("To join, please enable video rooms in Labs first");
joinButtons = <AccessibleButton kind="primary" onClick={viewLabs}>
{ _t("Show Labs settings") }
</AccessibleButton>;
}
return <div className="mx_RoomPreviewCard">
{ inviterSection }
<div className="mx_RoomPreviewCard_avatar">
{ avatarRow }
</div>
<h1 className="mx_RoomPreviewCard_name">
<RoomName room={room} />
</h1>
<RoomInfoLine room={room} />
<RoomTopic room={room}>
{ (topic, ref) =>
topic ? <div className="mx_RoomPreviewCard_topic" ref={ref}>
{ topic }
</div> : null
}
</RoomTopic>
{ room.getJoinRule() === "public" && <RoomFacePile room={room} /> }
{ notice ? <div className="mx_RoomPreviewCard_notice">
{ notice }
</div> : null }
<div className="mx_RoomPreviewCard_joinButtons">
{ joinButtons }
</div>
</div>;
};
export default RoomPreviewCard;