Initial SpaceRoomView work
This commit is contained in:
parent
c8fe3f7676
commit
c10512fd56
19 changed files with 1066 additions and 45 deletions
|
@ -28,6 +28,7 @@
|
|||
@import "./structures/_ScrollPanel.scss";
|
||||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_SpacePanel.scss";
|
||||
@import "./structures/_SpaceRoomView.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
|
@ -235,6 +236,7 @@
|
|||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||
@import "./views/spaces/_SpaceBasicSettings.scss";
|
||||
@import "./views/spaces/_SpaceCreateMenu.scss";
|
||||
@import "./views/spaces/_SpacePublicShare.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
@import "./views/toasts/_AnalyticsToast.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
|
|
244
res/css/structures/_SpaceRoomView.scss
Normal file
244
res/css/structures/_SpaceRoomView.scss
Normal file
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
$SpaceRoomViewInnerWidth: 428px;
|
||||
|
||||
.mx_SpaceRoomView {
|
||||
.mx_MainSplit > div:first-child {
|
||||
padding: 80px 60px;
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: $font-24px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-fg-color;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_description {
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_buttons {
|
||||
display: block;
|
||||
margin-top: 44px;
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
text-align: right; // button alignment right
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
max-width: $SpaceRoomViewInnerWidth;
|
||||
|
||||
& + .mx_Field {
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_errorText {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $notice-primary-color;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing {
|
||||
overflow-y: auto;
|
||||
|
||||
> .mx_BaseAvatar_image,
|
||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_name {
|
||||
margin: 24px 0 16px;
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_nameRow {
|
||||
margin-top: 12px;
|
||||
|
||||
> h1 {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_inviter {
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_memberCount {
|
||||
position: relative;
|
||||
margin-left: 24px;
|
||||
padding: 0 0 0 28px;
|
||||
line-height: $font-24px;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
background-color: $accent-color;
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_topic {
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_landing_joinButtons {
|
||||
margin-top: 24px;
|
||||
|
||||
.mx_FormButton {
|
||||
padding: 8px 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope {
|
||||
.mx_RadioButton {
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $space-button-outline-color;
|
||||
padding: 16px 16px 16px 72px;
|
||||
margin-top: 36px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
> div:first-of-type {
|
||||
// hide radio dot
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_RadioButton_content {
|
||||
margin: 0;
|
||||
|
||||
> h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: $font-15px;
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
> div {
|
||||
color: $secondary-fg-color;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
top: 24px;
|
||||
left: 20px;
|
||||
background-color: $secondary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RadioButton_checked {
|
||||
border-color: $accent-color;
|
||||
|
||||
.mx_RadioButton_content {
|
||||
> div {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope_justMeButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates {
|
||||
.mx_SpaceRoomView_inviteTeammates_buttons {
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 28px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 32px;
|
||||
line-height: 24px; // to center icons
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $secondary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
margin-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
res/css/views/spaces/_SpacePublicShare.scss
Normal file
60
res/css/views/spaces/_SpacePublicShare.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2021 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_SpacePublicShare {
|
||||
.mx_AccessibleButton {
|
||||
border: 1px solid $space-button-outline-color;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px 12px 52px;
|
||||
margin-top: 16px;
|
||||
width: $SpaceRoomViewInnerWidth;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
color: #368bd6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
background: $muted-fg-color;
|
||||
left: 12px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
&.mx_SpacePublicShare_shareButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||
}
|
||||
|
||||
&.mx_SpacePublicShare_inviteButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
}
|
3
res/img/element-icons/link.svg
Normal file
3
res/img/element-icons/link.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 549 B |
|
@ -123,6 +123,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -120,6 +120,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -187,6 +187,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: #E3E8F0;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -181,6 +181,7 @@ $roomsublist-divider-color: $primary-fg-color;
|
|||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
$space-button-outline-color: #E3E8F0;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
|
|||
import Modal from './Modal';
|
||||
import * as sdk from './';
|
||||
import { _t } from './languageHandler';
|
||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||
|
||||
|
@ -75,6 +75,13 @@ export function showCommunityInviteDialog(communityId) {
|
|||
}
|
||||
}
|
||||
|
||||
export const showSpaceInviteDialog = (roomId) => {
|
||||
Modal.createTrackedDialog("Invite Users", "Space", InviteDialog, {
|
||||
kind: KIND_SPACE_INVITE,
|
||||
roomId,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given MatrixEvent is a valid 3rd party user invite.
|
||||
* @param {MatrixEvent} event The event to check
|
||||
|
|
|
@ -24,7 +24,11 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||
import {
|
||||
RightPanelPhases,
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
RIGHT_PANEL_SPACE_PHASES,
|
||||
} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
@ -79,6 +83,8 @@ export default class RightPanel extends React.Component {
|
|||
return RightPanelPhases.GroupMemberList;
|
||||
}
|
||||
return rps.groupPanelPhase;
|
||||
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
|
||||
return RightPanelPhases.SpaceMemberList;
|
||||
} else if (userForPanel) {
|
||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||
// from its props and some from a store, except if the contents of the store changes
|
||||
|
@ -99,9 +105,8 @@ export default class RightPanel extends React.Component {
|
|||
return rps.roomPanelPhase;
|
||||
}
|
||||
return RightPanelPhases.RoomMemberInfo;
|
||||
} else {
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -181,6 +186,7 @@ export default class RightPanel extends React.Component {
|
|||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
widgetId: payload.widgetId,
|
||||
space: payload.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -232,6 +238,13 @@ export default class RightPanel extends React.Component {
|
|||
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
case RightPanelPhases.SpaceMemberList:
|
||||
panel = <MemberList
|
||||
roomId={this.state.space ? this.state.space.roomId : roomId}
|
||||
key={this.state.space ? this.state.space.roomId : roomId}
|
||||
onClose={this.onClose}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupMemberList:
|
||||
if (this.props.groupId) {
|
||||
|
@ -244,10 +257,11 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
case RightPanelPhases.EncryptionPanel:
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
room={this.props.room}
|
||||
room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
|
@ -257,6 +271,7 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
|
||||
case RightPanelPhases.Room3pidMemberInfo:
|
||||
case RightPanelPhases.Space3pidMemberInfo:
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
|
||||
break;
|
||||
|
||||
|
|
|
@ -1400,7 +1400,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onRejectButtonClicked = ev => {
|
||||
private onRejectButtonClicked = () => {
|
||||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
|
@ -1460,7 +1460,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRejectThreepidInviteButtonClicked = ev => {
|
||||
private onRejectThreepidInviteButtonClicked = () => {
|
||||
// We can reject 3pid invites in the same way that we accept them,
|
||||
// using /leave rather than /join. In the short term though, we
|
||||
// just ignore them.
|
||||
|
@ -1723,7 +1723,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership == 'invite') {
|
||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
@ -1852,7 +1852,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek) {
|
||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
{ previewBar }
|
||||
|
@ -1874,6 +1874,18 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.room?.isSpaceRoom()) {
|
||||
return <SpaceRoomView
|
||||
space={this.state.room}
|
||||
justCreatedOpts={this.props.justCreatedOpts}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onJoinButtonClicked={this.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.props.threepidInvite
|
||||
? this.onRejectThreepidInviteButtonClicked
|
||||
: this.onRejectButtonClicked}
|
||||
/>;
|
||||
}
|
||||
|
||||
const auxPanel = (
|
||||
<AuxPanel
|
||||
room={this.state.room}
|
||||
|
|
503
src/components/structures/SpaceRoomView.tsx
Normal file
503
src/components/structures/SpaceRoomView.tsx
Normal file
|
@ -0,0 +1,503 @@
|
|||
/*
|
||||
Copyright 2021 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, {RefObject, useContext, useRef, useState} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import {inviteMultipleToRoom, showSpaceInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier"
|
||||
import MainSplit from './MainSplit';
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import {ActionPayload} from "../../dispatcher/payloads";
|
||||
import RightPanel from "./RightPanel";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {useStateArray} from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
justCreatedOpts?: IOpts;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
onJoinButtonClicked(): void;
|
||||
onRejectButtonClicked(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
showRightPanel: boolean;
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
Landing,
|
||||
PublicCreateRooms,
|
||||
PublicShare,
|
||||
PrivateScope,
|
||||
PrivateInvite,
|
||||
PrivateCreateRooms,
|
||||
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());
|
||||
useEventEmitter(room, "Room.myMembership", () => {
|
||||
setMembership(room.getMyMembership());
|
||||
});
|
||||
return membership;
|
||||
};
|
||||
|
||||
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
const joinRule = space.getJoinRule();
|
||||
const userId = cli.getUserId();
|
||||
|
||||
let joinButtons;
|
||||
if (myMembership === "invite") {
|
||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
||||
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
|
||||
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
|
||||
{_t("Decline")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (myMembership !== "join" && joinRule === "public") {
|
||||
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
|
||||
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_landing">
|
||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<div className="mx_SpaceRoomView_landing_name">
|
||||
<RoomName room={space}>
|
||||
{(name) => {
|
||||
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
|
||||
<h1>{ name }</h1>
|
||||
<RoomMemberCount room={space}>
|
||||
{(count) => count > 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_landing_memberCount"
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomMemberList,
|
||||
refireParams: { space },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("%(count)s members", { count }) }
|
||||
</AccessibleButton>
|
||||
) : null}
|
||||
</RoomMemberCount>
|
||||
</div> };
|
||||
if (myMembership === "invite") {
|
||||
const inviteSender = space.getMember(userId)?.events.member?.getSender();
|
||||
const inviter = inviteSender && space.getMember(inviteSender);
|
||||
|
||||
if (inviteSender) {
|
||||
return _t("<inviter/> invited you to <name/>", {}, {
|
||||
name: tags.name,
|
||||
inviter: () => inviter
|
||||
? <span className="mx_SpaceRoomView_landing_inviter">
|
||||
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
|
||||
{ inviter.name }
|
||||
</span>
|
||||
: <span className="mx_SpaceRoomView_landing_inviter">
|
||||
{ inviteSender }
|
||||
</span>,
|
||||
}) as JSX.Element;
|
||||
} else {
|
||||
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
|
||||
}
|
||||
} else if (shouldShowSpaceSettings(cli, space)) {
|
||||
if (space.getJoinRule() === "public") {
|
||||
return _t("Your public space <name/>", {}, tags) as JSX.Element;
|
||||
} else {
|
||||
return _t("Your private space <name/>", {}, tags) as JSX.Element;
|
||||
}
|
||||
}
|
||||
return _t("Welcome to <name/>", {}, tags) as JSX.Element;
|
||||
}}
|
||||
</RoomName>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
{ joinButtons }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const numFields = 3;
|
||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||
// TODO vary default prefills for "Just Me" spaces
|
||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||
const name = "roomName" + i;
|
||||
return <Field
|
||||
key={name}
|
||||
name={name}
|
||||
type="text"
|
||||
label={_t("Room name")}
|
||||
placeholder={placeholders[i]}
|
||||
value={roomNames[i]}
|
||||
onChange={ev => setRoomName(i, ev.target.value)}
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
setError("");
|
||||
setBusy(true);
|
||||
try {
|
||||
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
|
||||
return createRoom({
|
||||
createOpts: {
|
||||
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: false,
|
||||
inlineErrors: true,
|
||||
parentSpace: space,
|
||||
});
|
||||
}));
|
||||
onFinished();
|
||||
} catch (e) {
|
||||
console.error("Failed to create initial space rooms", e);
|
||||
setError(_t("Failed to create initial space rooms"));
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let onClick = onFinished;
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (roomNames.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>{ title }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ description }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton
|
||||
label={buttonLabel}
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPublicShare = ({ space, onFinished }) => {
|
||||
return <div className="mx_SpaceRoomView_publicShare">
|
||||
<h1>{ _t("Share your public space") }</h1>
|
||||
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
|
||||
|
||||
<SpacePublicShare space={space} onFinished={onFinished} />
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Finish")} onClick={onFinished} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPrivateScope = ({ onFinished }) => {
|
||||
const [option, setOption] = useState<string>(null);
|
||||
|
||||
return <div className="mx_SpaceRoomView_privateScope">
|
||||
<h1>{ _t("Who are you working with?") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="privateSpaceScope"
|
||||
value={option}
|
||||
onChange={setOption}
|
||||
definitions={[
|
||||
{
|
||||
value: "justMe",
|
||||
className: "mx_SpaceRoomView_privateScope_justMeButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Just Me") }</h3>
|
||||
<div>{ _t("A private space just for you") }</div>
|
||||
</React.Fragment>,
|
||||
}, {
|
||||
value: "meAndMyTeammates",
|
||||
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</React.Fragment>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const validateEmailRules = withValidation({
|
||||
rules: [{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
}],
|
||||
});
|
||||
|
||||
const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const numFields = 3;
|
||||
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
||||
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||
const name = "emailAddress" + i;
|
||||
return <Field
|
||||
key={name}
|
||||
name={name}
|
||||
type="text"
|
||||
label={_t("Email address")}
|
||||
placeholder={_t("Email")}
|
||||
value={emailAddresses[i]}
|
||||
onChange={ev => setEmailAddress(i, ev.target.value)}
|
||||
ref={fieldRefs[i]}
|
||||
onValidate={validateEmailRules}
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
setError("");
|
||||
for (let i = 0; i < fieldRefs.length; i++) {
|
||||
const fieldRef = fieldRefs[i];
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: true });
|
||||
|
||||
if (valid === false) { // true/null are allowed
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
const targetIds = emailAddresses.map(name => name.trim()).filter(Boolean);
|
||||
try {
|
||||
const result = await inviteMultipleToRoom(space.roomId, targetIds);
|
||||
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users to space: ", result);
|
||||
setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}));
|
||||
} else {
|
||||
onFinished();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to invite users to space: ", err);
|
||||
setError(_t("We couldn't invite those users. Please check the users you want to invite and try again."));
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
return <div className="mx_SpaceRoomView_inviteTeammates">
|
||||
<h1>{ _t("Invite your teammates") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
|
||||
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
|
||||
onClick={() => showSpaceInviteDialog(space.roomId)}
|
||||
>
|
||||
{ _t("Invite by username") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
|
||||
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly creator: string;
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
let phase = Phase.Landing;
|
||||
|
||||
this.creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const showSetup = this.props.justCreatedOpts && this.context.getUserId() === this.creator;
|
||||
|
||||
if (showSetup) {
|
||||
phase = this.props.justCreatedOpts.createOpts.preset === Preset.PublicChat
|
||||
? Phase.PublicCreateRooms : Phase.PrivateScope;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
phase,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.rightPanelStoreToken.remove();
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return;
|
||||
|
||||
if (payload.action === Action.ViewUser && payload.member) {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberInfo,
|
||||
refireParams: {
|
||||
space: this.props.space,
|
||||
member: payload.member,
|
||||
},
|
||||
});
|
||||
} else if (payload.action === "view_3pid_invite" && payload.event) {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.Space3pidMemberInfo,
|
||||
refireParams: {
|
||||
space: this.props.space,
|
||||
event: payload.event,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: this.props.space },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private renderBody() {
|
||||
switch (this.state.phase) {
|
||||
case Phase.Landing:
|
||||
return <SpaceLanding
|
||||
space={this.props.space}
|
||||
onJoinButtonClicked={this.props.onJoinButtonClicked}
|
||||
onRejectButtonClicked={this.props.onRejectButtonClicked}
|
||||
/>;
|
||||
|
||||
case Phase.PublicCreateRooms:
|
||||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What discussions do you want to have?")}
|
||||
description={_t("We'll create rooms for each topic.")}
|
||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
||||
/>;
|
||||
case Phase.PublicShare:
|
||||
return <SpaceSetupPublicShare
|
||||
space={this.props.space}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
|
||||
case Phase.PrivateScope:
|
||||
return <SpaceSetupPrivateScope
|
||||
onFinished={(invite: boolean) => {
|
||||
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
|
||||
}}
|
||||
/>;
|
||||
case Phase.PrivateInvite:
|
||||
return <SpaceSetupPrivateInvite
|
||||
space={this.props.space}
|
||||
onFinished={() => this.setState({ phase: Phase.PrivateCreateRooms })}
|
||||
/>;
|
||||
case Phase.PrivateCreateRooms:
|
||||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What projects are you working on?")}
|
||||
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const rightPanel = this.state.showRightPanel && this.state.phase === Phase.Landing
|
||||
? <RightPanel room={this.props.space} resizeNotifier={this.props.resizeNotifier} />
|
||||
: null;
|
||||
|
||||
return <main className="mx_SpaceRoomView">
|
||||
<ErrorBoundary>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
{ this.renderBody() }
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
|
@ -48,6 +48,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_SPACE_INVITE = "space_invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
|
@ -309,7 +310,7 @@ interface IInviteDialogProps {
|
|||
// not provided.
|
||||
kind: string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
|
@ -348,8 +349,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
@ -1026,7 +1027,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
}
|
||||
|
||||
|
@ -1247,37 +1248,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
title = _t("Invite to this room");
|
||||
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
|
||||
|
||||
let helpTextUntranslated;
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (identityServersEnabled) {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, email address, username (like <userId/>) or " +
|
||||
"<a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
} else {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
}
|
||||
} else { // KIND_SPACE_INVITE
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
}
|
||||
}
|
||||
|
||||
helpText = _t(helpTextUntranslated, {}, {
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
|
|
65
src/components/views/spaces/SpacePublicShare.tsx
Normal file
65
src/components/views/spaces/SpacePublicShare.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2021 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, {useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {showSpaceInviteDialog} from "../../../RoomInvite";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
||||
const [copiedText, setCopiedText] = useState(_t("Click to copy"));
|
||||
|
||||
return <div className="mx_SpacePublicShare">
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_shareButton"
|
||||
onClick={async () => {
|
||||
const permalinkCreator = new RoomPermalinkCreator(space);
|
||||
permalinkCreator.load();
|
||||
const success = await copyPlaintext(permalinkCreator.forRoom());
|
||||
const text = success ? _t("Copied!") : _t("Failed to copy");
|
||||
setCopiedText(text);
|
||||
await sleep(10);
|
||||
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
|
||||
setCopiedText(_t("Click to copy"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ _t("Share invite link") }
|
||||
<span>{ copiedText }</span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
showSpaceInviteDialog(space.roomId);
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Invite by email or username") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpacePublicShare;
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import { ActionPayload } from "../payloads";
|
||||
|
@ -35,4 +36,5 @@ export interface SetRightPanelPhaseRefireParams {
|
|||
// XXX: The type for event should 'view_3pid_invite' action's payload
|
||||
event?: any;
|
||||
widgetId?: string;
|
||||
space?: Room;
|
||||
}
|
||||
|
|
29
src/hooks/useStateArray.ts
Normal file
29
src/hooks/useStateArray.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2021 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 {useState} from "react";
|
||||
|
||||
// Hook to simplify managing state of arrays of a common type
|
||||
export const useStateArray = <T>(initialSize: number, initialState: T | T[]): [T[], (i: number, v: T) => void] => {
|
||||
const [data, setData] = useState<T[]>(() => {
|
||||
return Array.isArray(initialState) ? initialState : new Array(initialSize).fill(initialState);
|
||||
});
|
||||
return [data, (index: number, value: T) => setData(data => {
|
||||
const copy = [...data];
|
||||
copy[index] = value;
|
||||
return copy;
|
||||
})]
|
||||
};
|
|
@ -998,6 +998,11 @@
|
|||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"Home": "Home",
|
||||
"Click to copy": "Click to copy",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Share invite link": "Share invite link",
|
||||
"Invite by email or username": "Invite by email or username",
|
||||
"Remove": "Remove",
|
||||
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
||||
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
||||
|
@ -1814,8 +1819,6 @@
|
|||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||
"Click here to see older messages.": "Click here to see older messages.",
|
||||
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Add an Integration": "Add an Integration",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
|
||||
"Edited at %(date)s": "Edited at %(date)s",
|
||||
|
@ -2164,8 +2167,11 @@
|
|||
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
|
||||
"Go": "Go",
|
||||
"Invite to this space": "Invite to this space",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
|
||||
"Transfer": "Transfer",
|
||||
"a new master key signature": "a new master key signature",
|
||||
"a new cross-signing key signature": "a new cross-signing key signature",
|
||||
|
@ -2550,6 +2556,37 @@
|
|||
"Failed to reject invite": "Failed to reject invite",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
||||
"Accept Invite": "Accept Invite",
|
||||
"%(count)s members|other": "%(count)s members",
|
||||
"%(count)s members|one": "%(count)s member",
|
||||
"<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
|
||||
"You have been invited to <name/>": "You have been invited to <name/>",
|
||||
"Your public space <name/>": "Your public space <name/>",
|
||||
"Your private space <name/>": "Your private space <name/>",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
"Room name": "Room name",
|
||||
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
||||
"Skip for now": "Skip for now",
|
||||
"Creating rooms...": "Creating rooms...",
|
||||
"Share your public space": "Share your public space",
|
||||
"At the moment only you can see it.": "At the moment only you can see it.",
|
||||
"Finish": "Finish",
|
||||
"Who are you working with?": "Who are you working with?",
|
||||
"Ensure the right people have access to the space.": "Ensure the right people have access to the space.",
|
||||
"Just Me": "Just Me",
|
||||
"A private space just for you": "A private space just for you",
|
||||
"Me and my teammates": "Me and my teammates",
|
||||
"A private space for you and your teammates": "A private space for you and your teammates",
|
||||
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
"Invite by username": "Invite by username",
|
||||
"Inviting...": "Inviting...",
|
||||
"What discussions do you want to have?": "What discussions do you want to have?",
|
||||
"We'll create rooms for each topic.": "We'll create rooms for each topic.",
|
||||
"What projects are you working on?": "What projects are you working on?",
|
||||
"We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
|
|
|
@ -31,6 +31,11 @@ export enum RightPanelPhases {
|
|||
GroupRoomList = 'GroupRoomList',
|
||||
GroupRoomInfo = 'GroupRoomInfo',
|
||||
GroupMemberInfo = 'GroupMemberInfo',
|
||||
|
||||
// Space stuff
|
||||
SpaceMemberList = "SpaceMemberList",
|
||||
SpaceMemberInfo = "SpaceMemberInfo",
|
||||
Space3pidMemberInfo = "Space3pidMemberInfo",
|
||||
}
|
||||
|
||||
// These are the phases that are safe to persist (the ones that don't require additional
|
||||
|
@ -43,3 +48,10 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
|
|||
RightPanelPhases.GroupMemberList,
|
||||
RightPanelPhases.GroupRoomList,
|
||||
];
|
||||
|
||||
// Subset of phases visible in the Space View
|
||||
export const RIGHT_PANEL_SPACE_PHASES = [
|
||||
RightPanelPhases.SpaceMemberList,
|
||||
RightPanelPhases.Space3pidMemberInfo,
|
||||
RightPanelPhases.SpaceMemberInfo,
|
||||
];
|
||||
|
|
28
src/utils/space.ts
Normal file
28
src/utils/space.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2021 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 {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
|
||||
const userId = cli.getUserId();
|
||||
return space.getMyMembership() === "join"
|
||||
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
|
||||
};
|
Loading…
Reference in a new issue