mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 09:15:41 +03:00
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
|
@ -256,9 +256,11 @@
|
|||
@import "./views/rooms/_ReplyTile.scss";
|
||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||
@import "./views/rooms/_RoomHeader.scss";
|
||||
@import "./views/rooms/_RoomInfoLine.scss";
|
||||
@import "./views/rooms/_RoomList.scss";
|
||||
@import "./views/rooms/_RoomListHeader.scss";
|
||||
@import "./views/rooms/_RoomPreviewBar.scss";
|
||||
@import "./views/rooms/_RoomPreviewCard.scss";
|
||||
@import "./views/rooms/_RoomSublist.scss";
|
||||
@import "./views/rooms/_RoomTile.scss";
|
||||
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
||||
|
|
|
@ -137,124 +137,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview,
|
||||
.mx_SpaceRoomView_landing {
|
||||
.mx_SpaceRoomView_info_memberCount {
|
||||
color: inherit;
|
||||
position: relative;
|
||||
padding: 0 0 0 16px;
|
||||
font-size: $font-15px;
|
||||
display: inline; // cancel inline-flex
|
||||
|
||||
&::before {
|
||||
content: "·"; // visual separator
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview {
|
||||
padding: 32px 24px !important; // override default padding from above
|
||||
margin: auto;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 2px 15px 30px $dialog-shadow-color;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_BetaCard_betaPill {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_SpaceRoomView_preview_spaceBetaPrompt {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-content;
|
||||
margin-top: 24px;
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: $font-24px;
|
||||
width: 20px;
|
||||
left: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_inviter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: $font-15px;
|
||||
|
||||
> div {
|
||||
margin-left: 8px;
|
||||
|
||||
.mx_SpaceRoomView_preview_inviter_name {
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_inviter_mxid {
|
||||
line-height: $font-24px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .mx_RoomAvatar_isSpaceRoom {
|
||||
&.mx_BaseAvatar_image, .mx_BaseAvatar_image {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h1.mx_SpaceRoomView_preview_name {
|
||||
margin: 20px 0 !important; // override default margin from above
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_topic {
|
||||
font-size: $font-14px;
|
||||
line-height: $font-22px;
|
||||
color: $secondary-content;
|
||||
margin: 20px 0;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_joinButtons {
|
||||
margin-top: 20px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 0;
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -314,40 +196,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
flex-wrap: wrap;
|
||||
line-height: $font-24px;
|
||||
|
||||
.mx_SpaceRoomView_info {
|
||||
color: $secondary-content;
|
||||
font-size: $font-15px;
|
||||
display: inline-block;
|
||||
|
||||
.mx_SpaceRoomView_info_public,
|
||||
.mx_SpaceRoomView_info_private {
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $tertiary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_info_public::before {
|
||||
mask-size: 12px;
|
||||
mask-image: url("$(res)/img/globe.svg");
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_info_private::before {
|
||||
mask-size: 14px;
|
||||
mask-image: url("$(res)/img/element-icons/lock.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_infoBar_interactive {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -24,8 +24,7 @@ limitations under the License.
|
|||
margin-right: calc($container-gap-width / 2);
|
||||
|
||||
background-color: $header-panel-bg-color;
|
||||
padding-top: 33px; // to match the right panel chat heading
|
||||
border: 8px solid $header-panel-bg-color;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
.mx_AppTile {
|
||||
|
|
|
@ -15,6 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_FacePile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_FacePile_faces {
|
||||
display: inline-flex;
|
||||
flex-direction: row-reverse;
|
||||
|
|
58
res/css/views/rooms/_RoomInfoLine.scss
Normal file
58
res/css/views/rooms/_RoomInfoLine.scss
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_RoomInfoLine {
|
||||
color: $secondary-content;
|
||||
display: inline-block;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 1.2em;
|
||||
mask-position-y: center;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $tertiary-content;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&.mx_RoomInfoLine_public::before {
|
||||
width: 12px;
|
||||
mask-size: 12px;
|
||||
mask-image: url("$(res)/img/globe.svg");
|
||||
}
|
||||
|
||||
&.mx_RoomInfoLine_private::before {
|
||||
width: 14px;
|
||||
mask-size: 14px;
|
||||
mask-image: url("$(res)/img/element-icons/lock.svg");
|
||||
}
|
||||
|
||||
&.mx_RoomInfoLine_video::before {
|
||||
width: 16px;
|
||||
mask-size: 16px;
|
||||
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
||||
}
|
||||
|
||||
.mx_RoomInfoLine_members {
|
||||
color: inherit;
|
||||
|
||||
&::before {
|
||||
content: "·"; // visual separator
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
136
res/css/views/rooms/_RoomPreviewCard.scss
Normal file
136
res/css/views/rooms/_RoomPreviewCard.scss
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_RoomPreviewCard {
|
||||
padding: $spacing-32 $spacing-24 !important; // Override SpaceRoomView's default padding
|
||||
margin: auto;
|
||||
flex-grow: 1;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
background-color: $system;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
font-size: $font-14px;
|
||||
|
||||
.mx_RoomPreviewCard_notice {
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: $font-24px;
|
||||
color: $primary-content;
|
||||
margin-top: $spacing-24;
|
||||
position: relative;
|
||||
padding-left: calc(20px + $spacing-8);
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: $font-24px;
|
||||
width: 20px;
|
||||
left: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_inviter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-20;
|
||||
font-size: $font-15px;
|
||||
|
||||
> div {
|
||||
margin-left: $spacing-8;
|
||||
|
||||
.mx_RoomPreviewCard_inviter_name {
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_inviter_mxid {
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_RoomAvatar_isSpaceRoom {
|
||||
&.mx_BaseAvatar_image, .mx_BaseAvatar_image {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_video {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: calc((50px + 2 * 3px) / 2);
|
||||
background-color: $accent;
|
||||
border: 3px solid $system;
|
||||
|
||||
position: relative;
|
||||
left: calc(-50px / 4 - 3px);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
background-color: $button-primary-fg-color;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
mask-size: 22px;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1.mx_RoomPreviewCard_name {
|
||||
margin: $spacing-16 0 !important; // Override SpaceRoomView's default margins
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_topic {
|
||||
line-height: $font-22px;
|
||||
margin-top: $spacing-16;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_FacePile {
|
||||
margin-top: $spacing-20;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewCard_joinButtons {
|
||||
margin-top: $spacing-20;
|
||||
display: flex;
|
||||
gap: $spacing-20;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
max-width: 200px;
|
||||
padding: 14px 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -132,8 +132,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
events: {
|
||||
// Allow all users to send video member updates
|
||||
[VIDEO_CHANNEL_MEMBER]: 0,
|
||||
// Make widgets immutable, even to admins
|
||||
"im.vector.modular.widgets": 200,
|
||||
// Annoyingly, we have to reiterate all the defaults here
|
||||
[EventType.RoomName]: 50,
|
||||
[EventType.RoomAvatar]: 50,
|
||||
|
@ -144,10 +142,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
[EventType.RoomServerAcl]: 100,
|
||||
[EventType.RoomEncryption]: 100,
|
||||
},
|
||||
users: {
|
||||
// Temporarily give ourselves the power to set up a widget
|
||||
[client.getUserId()]: 200,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -270,11 +264,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
if (opts.roomType === RoomType.ElementVideo) {
|
||||
// Set up video rooms with a Jitsi widget
|
||||
await addVideoChannel(roomId, createOpts.name);
|
||||
|
||||
// Reset our power level back to admin so that the widget becomes immutable
|
||||
const room = client.getRoom(roomId);
|
||||
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent);
|
||||
}
|
||||
}).then(function() {
|
||||
// NB createRoom doesn't block on the client seeing the echo that the
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { throttle } from "lodash";
|
||||
|
@ -23,7 +23,7 @@ import { throttle } from "lodash";
|
|||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
|
||||
// Hook to simplify watching Matrix Room joined members
|
||||
export const useRoomMembers = (room: Room, throttleWait = 250) => {
|
||||
export const useRoomMembers = (room: Room, throttleWait = 250): RoomMember[] => {
|
||||
const [members, setMembers] = useState<RoomMember[]>(room.getJoinedMembers());
|
||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
||||
setMembers(room.getJoinedMembers());
|
||||
|
@ -32,10 +32,19 @@ export const useRoomMembers = (room: Room, throttleWait = 250) => {
|
|||
};
|
||||
|
||||
// Hook to simplify watching Matrix Room joined member count
|
||||
export const useRoomMemberCount = (room: Room, throttleWait = 250) => {
|
||||
export const useRoomMemberCount = (room: Room, throttleWait = 250): number => {
|
||||
const [count, setCount] = useState<number>(room.getJoinedMemberCount());
|
||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
||||
setCount(room.getJoinedMemberCount());
|
||||
}, throttleWait, { leading: true, trailing: true }));
|
||||
return count;
|
||||
};
|
||||
|
||||
// Hook to simplify watching the local user's membership in a room
|
||||
export const useMyRoomMembership = (room: Room): string => {
|
||||
const [membership, setMembership] = useState<string>(room.getMyMembership());
|
||||
useTypedEventEmitter(room, RoomEvent.MyMembership, () => {
|
||||
setMembership(room.getMyMembership());
|
||||
});
|
||||
return membership;
|
||||
};
|
||||
|
|
|
@ -1784,6 +1784,13 @@
|
|||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Invite": "Invite",
|
||||
"Video room": "Video room",
|
||||
"Public space": "Public space",
|
||||
"Public room": "Public room",
|
||||
"Private space": "Private space",
|
||||
"Private room": "Private room",
|
||||
"%(count)s members|other": "%(count)s members",
|
||||
"%(count)s members|one": "%(count)s member",
|
||||
"Start new chat": "Start new chat",
|
||||
"Invite to space": "Invite to space",
|
||||
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
||||
|
@ -1792,7 +1799,6 @@
|
|||
"Explore rooms": "Explore rooms",
|
||||
"New room": "New room",
|
||||
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
|
||||
"New video room": "New video room",
|
||||
"Add existing room": "Add existing room",
|
||||
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
|
||||
"Explore public rooms": "Explore public rooms",
|
||||
|
@ -1865,6 +1871,12 @@
|
|||
"This room or space is not accessible at this time.": "This room or space is not accessible at this time.",
|
||||
"Try again later, or ask a room or space admin to check if you have access.": "Try again later, or ask a room or space admin to check if you have access.",
|
||||
"%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
||||
"Leave": "Leave",
|
||||
"<inviter/> invites you": "<inviter/> invites you",
|
||||
"To view %(roomName)s, you need an invite": "To view %(roomName)s, you need an invite",
|
||||
"To view, please enable video rooms in Labs first": "To view, please enable video rooms in Labs first",
|
||||
"To join, please enable video rooms in Labs first": "To join, please enable video rooms in Labs first",
|
||||
"Show Labs settings": "Show Labs settings",
|
||||
"Appearance": "Appearance",
|
||||
"Show rooms with unread messages first": "Show rooms with unread messages first",
|
||||
"Show previews of messages": "Show previews of messages",
|
||||
|
@ -1883,7 +1895,6 @@
|
|||
"Favourite": "Favourite",
|
||||
"Low Priority": "Low Priority",
|
||||
"Copy room link": "Copy room link",
|
||||
"Leave": "Leave",
|
||||
"Video": "Video",
|
||||
"Connecting...": "Connecting...",
|
||||
"Connected": "Connected",
|
||||
|
@ -2471,7 +2482,6 @@
|
|||
"Topic (optional)": "Topic (optional)",
|
||||
"Room visibility": "Room visibility",
|
||||
"Private room (invite only)": "Private room (invite only)",
|
||||
"Public room": "Public room",
|
||||
"Visible to space members": "Visible to space members",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
"Create video room": "Create video room",
|
||||
|
@ -2482,7 +2492,6 @@
|
|||
"Add a space to a space you manage.": "Add a space to a space you manage.",
|
||||
"Space visibility": "Space visibility",
|
||||
"Private space (invite only)": "Private space (invite only)",
|
||||
"Public space": "Public space",
|
||||
"Want to add an existing space instead?": "Want to add an existing space instead?",
|
||||
"Adding...": "Adding...",
|
||||
"Sign out": "Sign out",
|
||||
|
@ -2650,8 +2659,6 @@
|
|||
"Manually export keys": "Manually export keys",
|
||||
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
||||
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
|
||||
"%(count)s members|other": "%(count)s members",
|
||||
"%(count)s members|one": "%(count)s member",
|
||||
"%(count)s rooms|other": "%(count)s rooms",
|
||||
"%(count)s rooms|one": "%(count)s room",
|
||||
"You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only",
|
||||
|
@ -3115,9 +3122,6 @@
|
|||
"Results": "Results",
|
||||
"Rooms and spaces": "Rooms and spaces",
|
||||
"Search names and descriptions": "Search names and descriptions",
|
||||
"Private space": "Private space",
|
||||
"<inviter/> invites you": "<inviter/> invites you",
|
||||
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
|
|
|
@ -17,9 +17,17 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
|
||||
import { stubClient, stubVideoChannelStore, mkRoom, wrapInMatrixClientContext } from "../../test-utils";
|
||||
import {
|
||||
stubClient,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoom,
|
||||
wrapInMatrixClientContext,
|
||||
} from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { VIDEO_CHANNEL } from "../../../src/utils/VideoChannelUtils";
|
||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||
|
@ -30,7 +38,6 @@ import AppTile from "../../../src/components/views/elements/AppTile";
|
|||
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
||||
|
||||
describe("VideoRoomView", () => {
|
||||
stubClient();
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
||||
id: VIDEO_CHANNEL,
|
||||
eventId: "$1:example.org",
|
||||
|
@ -45,22 +52,22 @@ describe("VideoRoomView", () => {
|
|||
value: { enumerateDevices: () => [] },
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = mkRoom(cli, "!1:example.org");
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
let store: StubVideoChannelStore;
|
||||
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
|
||||
store = stubVideoChannelStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
});
|
||||
|
||||
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(async () => Promise.resolve());
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(true);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
|
@ -70,7 +77,7 @@ describe("VideoRoomView", () => {
|
|||
store.connect("!1:example.org");
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(async () => Promise.resolve());
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(false);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
|
|
|
@ -37,35 +37,21 @@ describe("createRoom", () => {
|
|||
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
||||
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
|
||||
|
||||
const userId = client.getUserId();
|
||||
const roomId = await createRoom({ roomType: RoomType.ElementVideo });
|
||||
|
||||
const [[{
|
||||
power_level_content_override: {
|
||||
users: {
|
||||
[userId]: userPower,
|
||||
},
|
||||
events: {
|
||||
"im.vector.modular.widgets": widgetPower,
|
||||
[VIDEO_CHANNEL_MEMBER]: videoMemberPower,
|
||||
},
|
||||
events: { [VIDEO_CHANNEL_MEMBER]: videoMemberPower },
|
||||
},
|
||||
}]] = mocked(client.createRoom).mock.calls as any;
|
||||
}]] = mocked(client.createRoom).mock.calls as any; // no good type
|
||||
const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls;
|
||||
|
||||
// We should have had enough power to be able to set up the Jitsi widget
|
||||
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
|
||||
// and should have actually set it up
|
||||
// We should have set up the Jitsi widget
|
||||
expect(widgetRoomId).toEqual(roomId);
|
||||
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
||||
expect(widgetId).toEqual(VIDEO_CHANNEL);
|
||||
|
||||
// All members should be able to update their connected devices
|
||||
expect(videoMemberPower).toEqual(0);
|
||||
// Jitsi widget should be immutable for admins
|
||||
expect(widgetPower).toBeGreaterThan(100);
|
||||
// and we should have been reset back to admin
|
||||
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue