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/_ReplyTile.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
@import "./views/rooms/_RoomHeader.scss";
|
@import "./views/rooms/_RoomHeader.scss";
|
||||||
|
@import "./views/rooms/_RoomInfoLine.scss";
|
||||||
@import "./views/rooms/_RoomList.scss";
|
@import "./views/rooms/_RoomList.scss";
|
||||||
@import "./views/rooms/_RoomListHeader.scss";
|
@import "./views/rooms/_RoomListHeader.scss";
|
||||||
@import "./views/rooms/_RoomPreviewBar.scss";
|
@import "./views/rooms/_RoomPreviewBar.scss";
|
||||||
|
@import "./views/rooms/_RoomPreviewCard.scss";
|
||||||
@import "./views/rooms/_RoomSublist.scss";
|
@import "./views/rooms/_RoomSublist.scss";
|
||||||
@import "./views/rooms/_RoomTile.scss";
|
@import "./views/rooms/_RoomTile.scss";
|
||||||
@import "./views/rooms/_RoomUpgradeWarningBar.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 {
|
.mx_SpaceRoomView_landing {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -314,40 +196,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
line-height: $font-24px;
|
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 {
|
.mx_SpaceRoomView_landing_infoBar_interactive {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -24,8 +24,7 @@ limitations under the License.
|
||||||
margin-right: calc($container-gap-width / 2);
|
margin-right: calc($container-gap-width / 2);
|
||||||
|
|
||||||
background-color: $header-panel-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
padding-top: 33px; // to match the right panel chat heading
|
padding: 8px;
|
||||||
border: 8px solid $header-panel-bg-color;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.mx_AppTile {
|
.mx_AppTile {
|
||||||
|
|
|
@ -15,6 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_FacePile {
|
.mx_FacePile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.mx_FacePile_faces {
|
.mx_FacePile_faces {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row-reverse;
|
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 TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
|
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
|
@ -1831,6 +1832,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const myMembership = this.state.room.getMyMembership();
|
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
|
// SpaceRoomView handles invites itself
|
||||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) {
|
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) {
|
||||||
if (this.state.joining || this.state.rejecting) {
|
if (this.state.joining || this.state.rejecting) {
|
||||||
|
|
|
@ -26,13 +26,10 @@ import { _t } from "../../languageHandler";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import RoomTopic from "../views/elements/RoomTopic";
|
import RoomTopic from "../views/elements/RoomTopic";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
|
||||||
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||||
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
|
||||||
import { useFeatureEnabled } from "../../hooks/useSettings";
|
import { useFeatureEnabled } from "../../hooks/useSettings";
|
||||||
import createRoom, { IOpts } from "../../createRoom";
|
import createRoom, { IOpts } from "../../createRoom";
|
||||||
import Field from "../views/elements/Field";
|
import Field from "../views/elements/Field";
|
||||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
|
||||||
import withValidation from "../views/elements/Validation";
|
import withValidation from "../views/elements/Validation";
|
||||||
import * as Email from "../../email";
|
import * as Email from "../../email";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
@ -56,7 +53,6 @@ import {
|
||||||
showSpaceSettings,
|
showSpaceSettings,
|
||||||
} from "../../utils/space";
|
} from "../../utils/space";
|
||||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
|
||||||
import RoomFacePile from "../views/elements/RoomFacePile";
|
import RoomFacePile from "../views/elements/RoomFacePile";
|
||||||
import {
|
import {
|
||||||
AddExistingToSpace,
|
AddExistingToSpace,
|
||||||
|
@ -70,11 +66,10 @@ import IconizedContextMenu, {
|
||||||
} from "../views/context_menus/IconizedContextMenu";
|
} from "../views/context_menus/IconizedContextMenu";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { BetaPill } from "../views/beta/BetaCard";
|
import { BetaPill } from "../views/beta/BetaCard";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
|
||||||
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
||||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
import RoomInfoLine from "../views/rooms/RoomInfoLine";
|
||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||||
import { useRoomState } from "../../hooks/useRoomState";
|
import { useMyRoomMembership } from "../../hooks/useRoomMembers";
|
||||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||||
import { UIComponent } from "../../settings/UIFeature";
|
import { UIComponent } from "../../settings/UIFeature";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
|
@ -106,205 +101,6 @@ enum Phase {
|
||||||
PrivateExistingRooms,
|
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 SpaceLandingAddButton = ({ space }) => {
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
||||||
|
@ -339,7 +135,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||||
label={_t("New video room")}
|
label={_t("Video room")}
|
||||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -451,7 +247,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
||||||
</RoomName>
|
</RoomName>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomView_landing_infoBar">
|
<div className="mx_SpaceRoomView_landing_infoBar">
|
||||||
<SpaceInfo space={space} />
|
<RoomInfoLine room={space} />
|
||||||
<div className="mx_SpaceRoomView_landing_infoBar_interactive">
|
<div className="mx_SpaceRoomView_landing_infoBar_interactive">
|
||||||
<RoomFacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
<RoomFacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
|
@ -846,8 +642,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
if (this.state.myMembership === "join") {
|
if (this.state.myMembership === "join") {
|
||||||
return <SpaceLanding space={this.props.space} />;
|
return <SpaceLanding space={this.props.space} />;
|
||||||
} else {
|
} else {
|
||||||
return <SpacePreview
|
return <RoomPreviewCard
|
||||||
space={this.props.space}
|
room={this.props.space}
|
||||||
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
||||||
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -20,28 +20,47 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||||
import { getVideoChannel } from "../../utils/VideoChannelUtils";
|
import WidgetUtils from "../../utils/WidgetUtils";
|
||||||
import WidgetStore from "../../stores/WidgetStore";
|
import { addVideoChannel, getVideoChannel } from "../../utils/VideoChannelUtils";
|
||||||
|
import WidgetStore, { IApp } from "../../stores/WidgetStore";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
|
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
|
||||||
import AppTile from "../views/elements/AppTile";
|
import AppTile from "../views/elements/AppTile";
|
||||||
import VideoLobby from "../views/voip/VideoLobby";
|
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 cli = useContext(MatrixClientContext);
|
||||||
const store = VideoChannelStore.instance;
|
const store = VideoChannelStore.instance;
|
||||||
|
|
||||||
// In case we mount before the WidgetStore knows about our Jitsi widget
|
// 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);
|
const [widgetLoaded, setWidgetLoaded] = useState(false);
|
||||||
useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => {
|
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: IApp = useMemo(() => {
|
||||||
const app = getVideoChannel(room.roomId);
|
if (widgetStoreReady) {
|
||||||
if (!app) logger.warn(`No video channel for room ${room.roomId}`);
|
const app = getVideoChannel(room.roomId);
|
||||||
return app;
|
if (!app) {
|
||||||
}, [room, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
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);
|
const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId);
|
||||||
useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(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 React, { useContext } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
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 { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
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 hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
|
||||||
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
|
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
|
||||||
|
const canAddVideoRooms = canAddRooms && SettingsStore.getValue("feature_video_rooms");
|
||||||
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
|
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
|
||||||
|
|
||||||
let newRoomSection: JSX.Element;
|
let newRoomSection: JSX.Element;
|
||||||
|
@ -149,6 +150,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onNewVideoRoomClick = (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
showCreateNewRoom(space, RoomType.ElementVideo);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
const onNewSubspaceClick = (ev: ButtonEvent) => {
|
const onNewSubspaceClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -169,6 +178,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
|
||||||
onClick={onNewRoomClick}
|
onClick={onNewRoomClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{ canAddVideoRooms &&
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
data-test-id='new-video-room-option'
|
||||||
|
iconClassName="mx_SpacePanel_iconPlus"
|
||||||
|
label={_t("Video room")}
|
||||||
|
onClick={onNewVideoRoomClick}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{ canAddSubSpaces &&
|
{ canAddSubSpaces &&
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
data-test-id='new-subspace-option'
|
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 FacePile: FC<IProps> = ({ members, faceSize, overflow, tooltip, children, ...props }) => {
|
||||||
const faces = members.map(
|
const faces = members.map(
|
||||||
tooltip ?
|
tooltip
|
||||||
m => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} /> :
|
? m => <MemberAvatar key={m.userId} member={m} width={faceSize} height={faceSize} hideTitle />
|
||||||
m => <TooltipTarget key={m.userId} label={m.name}>
|
: m => <TooltipTarget key={m.userId} label={m.name}>
|
||||||
<MemberAvatar member={m} width={faceSize} height={faceSize} viewUserOnClick={!props.onClick} />
|
<MemberAvatar
|
||||||
|
member={m}
|
||||||
|
width={faceSize}
|
||||||
|
height={faceSize}
|
||||||
|
viewUserOnClick={!props.onClick}
|
||||||
|
hideTitle
|
||||||
|
/>
|
||||||
</TooltipTarget>,
|
</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")}
|
: _t("You do not have permissions to create new rooms in this space")}
|
||||||
/>
|
/>
|
||||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||||
label={_t("New video room")}
|
label={_t("Video room")}
|
||||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -283,7 +283,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||||
label={_t("New video room")}
|
label={_t("Video room")}
|
||||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -222,7 +222,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||||
/>
|
/>
|
||||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||||
label={_t("New video room")}
|
label={_t("Video room")}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -313,7 +313,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||||
label={_t("New video room")}
|
label={_t("Video room")}
|
||||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
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: {
|
events: {
|
||||||
// Allow all users to send video member updates
|
// Allow all users to send video member updates
|
||||||
[VIDEO_CHANNEL_MEMBER]: 0,
|
[VIDEO_CHANNEL_MEMBER]: 0,
|
||||||
// Make widgets immutable, even to admins
|
|
||||||
"im.vector.modular.widgets": 200,
|
|
||||||
// Annoyingly, we have to reiterate all the defaults here
|
// Annoyingly, we have to reiterate all the defaults here
|
||||||
[EventType.RoomName]: 50,
|
[EventType.RoomName]: 50,
|
||||||
[EventType.RoomAvatar]: 50,
|
[EventType.RoomAvatar]: 50,
|
||||||
|
@ -144,10 +142,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
[EventType.RoomServerAcl]: 100,
|
[EventType.RoomServerAcl]: 100,
|
||||||
[EventType.RoomEncryption]: 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) {
|
if (opts.roomType === RoomType.ElementVideo) {
|
||||||
// Set up video rooms with a Jitsi widget
|
// Set up video rooms with a Jitsi widget
|
||||||
await addVideoChannel(roomId, createOpts.name);
|
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() {
|
}).then(function() {
|
||||||
// NB createRoom doesn't block on the client seeing the echo that the
|
// 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 { 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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
|
@ -23,7 +23,7 @@ import { throttle } from "lodash";
|
||||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
// Hook to simplify watching Matrix Room joined members
|
// 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());
|
const [members, setMembers] = useState<RoomMember[]>(room.getJoinedMembers());
|
||||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
||||||
setMembers(room.getJoinedMembers());
|
setMembers(room.getJoinedMembers());
|
||||||
|
@ -32,10 +32,19 @@ export const useRoomMembers = (room: Room, throttleWait = 250) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hook to simplify watching Matrix Room joined member count
|
// 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());
|
const [count, setCount] = useState<number>(room.getJoinedMemberCount());
|
||||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
|
||||||
setCount(room.getJoinedMemberCount());
|
setCount(room.getJoinedMemberCount());
|
||||||
}, throttleWait, { leading: true, trailing: true }));
|
}, throttleWait, { leading: true, trailing: true }));
|
||||||
return count;
|
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",
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Invite": "Invite",
|
"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",
|
"Start new chat": "Start new chat",
|
||||||
"Invite to space": "Invite to space",
|
"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",
|
"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",
|
"Explore rooms": "Explore rooms",
|
||||||
"New room": "New room",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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.",
|
"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.",
|
"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>.",
|
"%(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",
|
"Appearance": "Appearance",
|
||||||
"Show rooms with unread messages first": "Show rooms with unread messages first",
|
"Show rooms with unread messages first": "Show rooms with unread messages first",
|
||||||
"Show previews of messages": "Show previews of messages",
|
"Show previews of messages": "Show previews of messages",
|
||||||
|
@ -1883,7 +1895,6 @@
|
||||||
"Favourite": "Favourite",
|
"Favourite": "Favourite",
|
||||||
"Low Priority": "Low Priority",
|
"Low Priority": "Low Priority",
|
||||||
"Copy room link": "Copy room link",
|
"Copy room link": "Copy room link",
|
||||||
"Leave": "Leave",
|
|
||||||
"Video": "Video",
|
"Video": "Video",
|
||||||
"Connecting...": "Connecting...",
|
"Connecting...": "Connecting...",
|
||||||
"Connected": "Connected",
|
"Connected": "Connected",
|
||||||
|
@ -2471,7 +2482,6 @@
|
||||||
"Topic (optional)": "Topic (optional)",
|
"Topic (optional)": "Topic (optional)",
|
||||||
"Room visibility": "Room visibility",
|
"Room visibility": "Room visibility",
|
||||||
"Private room (invite only)": "Private room (invite only)",
|
"Private room (invite only)": "Private room (invite only)",
|
||||||
"Public room": "Public room",
|
|
||||||
"Visible to space members": "Visible to space members",
|
"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.",
|
"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",
|
"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.",
|
"Add a space to a space you manage.": "Add a space to a space you manage.",
|
||||||
"Space visibility": "Space visibility",
|
"Space visibility": "Space visibility",
|
||||||
"Private space (invite only)": "Private space (invite only)",
|
"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?",
|
"Want to add an existing space instead?": "Want to add an existing space instead?",
|
||||||
"Adding...": "Adding...",
|
"Adding...": "Adding...",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
@ -2650,8 +2659,6 @@
|
||||||
"Manually export keys": "Manually export keys",
|
"Manually export keys": "Manually export keys",
|
||||||
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
"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?",
|
"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|other": "%(count)s rooms",
|
||||||
"%(count)s rooms|one": "%(count)s room",
|
"%(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",
|
"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",
|
"Results": "Results",
|
||||||
"Rooms and spaces": "Rooms and spaces",
|
"Rooms and spaces": "Rooms and spaces",
|
||||||
"Search names and descriptions": "Search names and descriptions",
|
"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/>",
|
"Welcome to <name/>": "Welcome to <name/>",
|
||||||
"Random": "Random",
|
"Random": "Random",
|
||||||
"Support": "Support",
|
"Support": "Support",
|
||||||
|
|
|
@ -17,9 +17,17 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { act } from "react-dom/test-utils";
|
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 { 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 { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import { VIDEO_CHANNEL } from "../../../src/utils/VideoChannelUtils";
|
import { VIDEO_CHANNEL } from "../../../src/utils/VideoChannelUtils";
|
||||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||||
|
@ -30,7 +38,6 @@ import AppTile from "../../../src/components/views/elements/AppTile";
|
||||||
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
||||||
|
|
||||||
describe("VideoRoomView", () => {
|
describe("VideoRoomView", () => {
|
||||||
stubClient();
|
|
||||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
||||||
id: VIDEO_CHANNEL,
|
id: VIDEO_CHANNEL,
|
||||||
eventId: "$1:example.org",
|
eventId: "$1:example.org",
|
||||||
|
@ -45,22 +52,22 @@ describe("VideoRoomView", () => {
|
||||||
value: { enumerateDevices: () => [] },
|
value: { enumerateDevices: () => [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
let cli: MatrixClient;
|
||||||
const room = mkRoom(cli, "!1:example.org");
|
let room: Room;
|
||||||
|
let store: StubVideoChannelStore;
|
||||||
|
|
||||||
let store;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
cli = MatrixClientPeg.get();
|
||||||
|
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
|
||||||
store = stubVideoChannelStore();
|
store = stubVideoChannelStore();
|
||||||
});
|
room = mkRoom(cli, "!1:example.org");
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||||
// Wait for state to settle
|
// Wait for state to settle
|
||||||
await act(async () => Promise.resolve());
|
await act(() => Promise.resolve());
|
||||||
|
|
||||||
expect(view.find(VideoLobby).exists()).toEqual(true);
|
expect(view.find(VideoLobby).exists()).toEqual(true);
|
||||||
expect(view.find(AppTile).exists()).toEqual(true);
|
expect(view.find(AppTile).exists()).toEqual(true);
|
||||||
|
@ -70,7 +77,7 @@ describe("VideoRoomView", () => {
|
||||||
store.connect("!1:example.org");
|
store.connect("!1:example.org");
|
||||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||||
// Wait for state to settle
|
// Wait for state to settle
|
||||||
await act(async () => Promise.resolve());
|
await act(() => Promise.resolve());
|
||||||
|
|
||||||
expect(view.find(VideoLobby).exists()).toEqual(false);
|
expect(view.find(VideoLobby).exists()).toEqual(false);
|
||||||
expect(view.find(AppTile).exists()).toEqual(true);
|
expect(view.find(AppTile).exists()).toEqual(true);
|
||||||
|
|
|
@ -37,35 +37,21 @@ describe("createRoom", () => {
|
||||||
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
||||||
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
|
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
|
||||||
|
|
||||||
const userId = client.getUserId();
|
|
||||||
const roomId = await createRoom({ roomType: RoomType.ElementVideo });
|
const roomId = await createRoom({ roomType: RoomType.ElementVideo });
|
||||||
|
|
||||||
const [[{
|
const [[{
|
||||||
power_level_content_override: {
|
power_level_content_override: {
|
||||||
users: {
|
events: { [VIDEO_CHANNEL_MEMBER]: videoMemberPower },
|
||||||
[userId]: userPower,
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
"im.vector.modular.widgets": widgetPower,
|
|
||||||
[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;
|
const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls;
|
||||||
|
|
||||||
// We should have had enough power to be able to set up the Jitsi widget
|
// We should have set up the Jitsi widget
|
||||||
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
|
|
||||||
// and should have actually set it up
|
|
||||||
expect(widgetRoomId).toEqual(roomId);
|
expect(widgetRoomId).toEqual(roomId);
|
||||||
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
||||||
expect(widgetId).toEqual(VIDEO_CHANNEL);
|
expect(widgetId).toEqual(VIDEO_CHANNEL);
|
||||||
|
|
||||||
// All members should be able to update their connected devices
|
// All members should be able to update their connected devices
|
||||||
expect(videoMemberPower).toEqual(0);
|
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