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:
parent
cda31136b9
commit
658ff4dfe6
20 changed files with 617 additions and 434 deletions
|
@ -65,6 +65,7 @@ import ScrollPanel from "./ScrollPanel";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
|
@ -1831,6 +1832,21 @@ 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")
|
||||
) {
|
||||
return <ErrorBoundary>
|
||||
<div className="mx_MainSplit">
|
||||
<RoomPreviewCard
|
||||
room={this.state.room}
|
||||
onJoinButtonClicked={this.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.onRejectButtonClicked}
|
||||
/>
|
||||
</div>;
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
|
||||
// SpaceRoomView handles invites itself
|
||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
|
|
|
@ -26,13 +26,10 @@ import { _t } from "../../languageHandler";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
||||
import { useFeatureEnabled } from "../../hooks/useSettings";
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -56,7 +53,6 @@ import {
|
|||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import RoomFacePile from "../views/elements/RoomFacePile";
|
||||
import {
|
||||
AddExistingToSpace,
|
||||
|
@ -70,11 +66,10 @@ import IconizedContextMenu, {
|
|||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { BetaPill } from "../views/beta/BetaCard";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import { useRoomState } from "../../hooks/useRoomState";
|
||||
import RoomInfoLine from "../views/rooms/RoomInfoLine";
|
||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import { useMyRoomMembership } from "../../hooks/useRoomMembers";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
|
@ -106,205 +101,6 @@ enum Phase {
|
|||
PrivateExistingRooms,
|
||||
}
|
||||
|
||||
const RoomMemberCount = ({ room, children }) => {
|
||||
const members = useRoomMembers(room);
|
||||
const count = members.length;
|
||||
|
||||
if (children) return children(count);
|
||||
return count;
|
||||
};
|
||||
|
||||
const useMyRoomMembership = (room: Room) => {
|
||||
const [membership, setMembership] = useState(room.getMyMembership());
|
||||
useTypedEventEmitter(room, RoomEvent.MyMembership, () => {
|
||||
setMembership(room.getMyMembership());
|
||||
});
|
||||
return membership;
|
||||
};
|
||||
|
||||
const SpaceInfo = ({ space }: { space: 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 (space.getMyMembership() !== "invite") return null;
|
||||
try {
|
||||
return space.client.getRoomSummary(space.roomId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [space]);
|
||||
const joinRule = useRoomState(space, state => state.getJoinRule());
|
||||
const membership = useMyRoomMembership(space);
|
||||
|
||||
let visibilitySection;
|
||||
if (joinRule === JoinRule.Public) {
|
||||
visibilitySection = <span className="mx_SpaceRoomView_info_public">
|
||||
{ _t("Public space") }
|
||||
</span>;
|
||||
} else {
|
||||
visibilitySection = <span className="mx_SpaceRoomView_info_private">
|
||||
{ _t("Private space") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
let memberSection;
|
||||
if (membership === "invite" && summary) {
|
||||
// Don't trust local state and instead use the summary API
|
||||
memberSection = <span className="mx_SpaceRoomView_info_memberCount">
|
||||
{ _t("%(count)s members", { count: summary.num_joined_members }) }
|
||||
</span>;
|
||||
} else if (summary !== undefined) { // summary is not still loading
|
||||
memberSection = <RoomMemberCount room={space}>
|
||||
{ (count) => count > 0 ? (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
className="mx_SpaceRoomView_info_memberCount"
|
||||
onClick={() => {
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList });
|
||||
}}
|
||||
>
|
||||
{ _t("%(count)s members", { count }) }
|
||||
</AccessibleButton>
|
||||
) : null }
|
||||
</RoomMemberCount>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_info">
|
||||
{ visibilitySection }
|
||||
{ memberSection }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface ISpacePreviewProps {
|
||||
space: Room;
|
||||
onJoinButtonClicked(): void;
|
||||
onRejectButtonClicked(): void;
|
||||
}
|
||||
|
||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
useDispatcher(defaultDispatcher, payload => {
|
||||
if (payload.action === Action.JoinRoomError && payload.roomId === space.roomId) {
|
||||
setBusy(false); // stop the spinner, join failed
|
||||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const joinRule = useRoomState(space, state => state.getJoinRule());
|
||||
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||
&& joinRule !== JoinRule.Public;
|
||||
|
||||
let inviterSection;
|
||||
let joinButtons;
|
||||
if (myMembership === "join") {
|
||||
// XXX remove this when spaces leaves Beta
|
||||
joinButtons = (
|
||||
<AccessibleButton
|
||||
kind="danger_outline"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (myMembership === "invite") {
|
||||
const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
|
||||
const inviter = inviteSender && space.getMember(inviteSender);
|
||||
|
||||
if (inviteSender) {
|
||||
inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
|
||||
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||
<div>
|
||||
<div className="mx_SpaceRoomView_preview_inviter_name">
|
||||
{ _t("<inviter/> invites you", {}, {
|
||||
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||
}) }
|
||||
</div>
|
||||
{ inviter ? <div className="mx_SpaceRoomView_preview_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 footer;
|
||||
if (cannotJoin) {
|
||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ _t("To view %(spaceName)s, you need an invite", {
|
||||
spaceName: space.name,
|
||||
}) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_preview">
|
||||
{ inviterSection }
|
||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<h1 className="mx_SpaceRoomView_preview_name">
|
||||
<RoomName room={space} />
|
||||
</h1>
|
||||
<SpaceInfo space={space} />
|
||||
<RoomTopic room={space}>
|
||||
{ (topic, ref) =>
|
||||
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
|
||||
{ topic }
|
||||
</div>
|
||||
}
|
||||
</RoomTopic>
|
||||
{ space.getJoinRule() === "public" && <RoomFacePile room={space} /> }
|
||||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
{ footer }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceLandingAddButton = ({ space }) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
||||
|
@ -339,7 +135,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
label={_t("Video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -451,7 +247,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
</RoomName>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_infoBar">
|
||||
<SpaceInfo space={space} />
|
||||
<RoomInfoLine room={space} />
|
||||
<div className="mx_SpaceRoomView_landing_infoBar_interactive">
|
||||
<RoomFacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
|
@ -846,8 +642,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
if (this.state.myMembership === "join") {
|
||||
return <SpaceLanding space={this.props.space} />;
|
||||
} else {
|
||||
return <SpacePreview
|
||||
space={this.props.space}
|
||||
return <RoomPreviewCard
|
||||
room={this.props.space}
|
||||
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
||||
/>;
|
||||
|
|
|
@ -20,28 +20,47 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||
import { getVideoChannel } from "../../utils/VideoChannelUtils";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../utils/WidgetUtils";
|
||||
import { addVideoChannel, getVideoChannel } from "../../utils/VideoChannelUtils";
|
||||
import WidgetStore, { IApp } from "../../stores/WidgetStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
|
||||
import AppTile from "../views/elements/AppTile";
|
||||
import VideoLobby from "../views/voip/VideoLobby";
|
||||
|
||||
const VideoRoomView: FC<{ room: Room, resizing: boolean }> = ({ room, resizing }) => {
|
||||
interface IProps {
|
||||
room: Room;
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
const VideoRoomView: FC<IProps> = ({ room, resizing }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const store = VideoChannelStore.instance;
|
||||
|
||||
// In case we mount before the WidgetStore knows about our Jitsi widget
|
||||
const [widgetStoreReady, setWidgetStoreReady] = useState(Boolean(WidgetStore.instance.matrixClient));
|
||||
const [widgetLoaded, setWidgetLoaded] = useState(false);
|
||||
useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => {
|
||||
if (roomId === null || roomId === room.roomId) setWidgetLoaded(true);
|
||||
if (roomId === null) setWidgetStoreReady(true);
|
||||
if (roomId === null || roomId === room.roomId) {
|
||||
setWidgetLoaded(Boolean(getVideoChannel(room.roomId)));
|
||||
}
|
||||
});
|
||||
|
||||
const app = useMemo(() => {
|
||||
const app = getVideoChannel(room.roomId);
|
||||
if (!app) logger.warn(`No video channel for room ${room.roomId}`);
|
||||
return app;
|
||||
}, [room, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const app: IApp = useMemo(() => {
|
||||
if (widgetStoreReady) {
|
||||
const app = getVideoChannel(room.roomId);
|
||||
if (!app) {
|
||||
logger.warn(`No video channel for room ${room.roomId}`);
|
||||
// Since widgets in video rooms are mutable, we'll take this opportunity to
|
||||
// reinstate the Jitsi widget in case another client removed it
|
||||
if (WidgetUtils.canUserModifyWidgets(room.roomId)) {
|
||||
addVideoChannel(room.roomId, room.name);
|
||||
}
|
||||
}
|
||||
return app;
|
||||
}
|
||||
}, [room, widgetStoreReady, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId);
|
||||
useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(store.roomId === room.roomId));
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>,
|
||||
);
|
||||
|
||||
|
|
86
src/components/views/rooms/RoomInfoLine.tsx
Normal file
86
src/components/views/rooms/RoomInfoLine.tsx
Normal 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;
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
202
src/components/views/rooms/RoomPreviewCard.tsx
Normal file
202
src/components/views/rooms/RoomPreviewCard.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue