Merge pull request #6543 from matrix-org/t3chguy/fix/18092
This commit is contained in:
commit
228d623024
22 changed files with 988 additions and 83 deletions
|
@ -75,6 +75,7 @@
|
|||
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_CreateSpaceFromCommunityDialog.scss";
|
||||
@import "./views/dialogs/_CreateSubspaceDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
|
|
|
@ -368,6 +368,65 @@ limitations under the License.
|
|||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.mx_GroupView_spaceUpgradePrompt {
|
||||
padding: 16px 50px;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 8px;
|
||||
max-width: 632px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
margin-top: 24px;
|
||||
position: relative;
|
||||
|
||||
> h2 {
|
||||
font-size: inherit;
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
|
||||
> p, h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: $font-24px;
|
||||
width: 20px;
|
||||
left: 18px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
background-color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_GroupView_spaceUpgradePrompt_close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: $input-darker-bg-color;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 8px;
|
||||
mask-image: url('$(res)/img/image-view/close.svg');
|
||||
background-color: $secondary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
|
|
|
@ -180,6 +180,18 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_migratedCommunity {
|
||||
margin-bottom: 16px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $input-border-color;
|
||||
width: max-content;
|
||||
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_preview_inviter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -342,7 +354,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
|
||||
.mx_SpaceFeedbackPrompt {
|
||||
padding: 7px; // 8px - 1px border
|
||||
border: 1px solid $menu-border-color;
|
||||
border: 1px solid rgba($primary-fg-color, .1);
|
||||
border-radius: 8px;
|
||||
width: max-content;
|
||||
margin: 0 0 -40px auto; // collapse its own height to not push other components down
|
||||
|
|
|
@ -51,6 +51,10 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/hide.svg');
|
||||
}
|
||||
|
||||
.mx_TagTileContextMenu_createSpace::before {
|
||||
mask-image: url('$(res)/img/element-icons/message/fwd.svg');
|
||||
}
|
||||
|
||||
.mx_TagTileContextMenu_separator {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
|
187
res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss
Normal file
187
res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
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_CreateSpaceFromCommunityDialog_wrapper {
|
||||
.mx_Dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog {
|
||||
width: 480px;
|
||||
color: $primary-fg-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
min-height: 0;
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_content {
|
||||
> p {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.mx_CreateSpaceFromCommunityDialog_flairNotice {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceBasicSettings {
|
||||
> p {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.mx_Field_textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_JoinRuleDropdown .mx_Dropdown_menu {
|
||||
width: auto !important; // override fixed width
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_nonPublicSpacer {
|
||||
height: 63px; // balance the height of the missing room alias field to prevent modal bouncing
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_footer {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
|
||||
> span {
|
||||
flex-grow: 1;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
|
||||
.mx_ProgressBar {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
|
||||
@mixin ProgressBarBorderRadius 8px;
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_progressText {
|
||||
margin-top: 8px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_error {
|
||||
padding-left: 12px;
|
||||
|
||||
> img {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_errorHeading {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-18px;
|
||||
color: $notice-primary-color;
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_errorCaption {
|
||||
margin-top: 4px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary {
|
||||
padding: 8px 36px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary_outline {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_retryButton {
|
||||
margin-left: 12px;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: $primary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog {
|
||||
.mx_InfoDialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
border: 3px solid $accent-color;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
margin: 12px auto 32px;
|
||||
|
||||
&::before {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: $accent-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
|
||||
mask-size: 48px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ limitations under the License.
|
|||
.mx_Field input,
|
||||
.mx_Field select,
|
||||
.mx_Field textarea {
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
font-size: $font-14px;
|
||||
border: none;
|
||||
|
|
|
@ -50,15 +50,21 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_SettingsTab_section {
|
||||
$right-gutter: 80px;
|
||||
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_SettingsFlag {
|
||||
margin-right: 80px;
|
||||
margin-right: $right-gutter;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin-right: $right-gutter;
|
||||
}
|
||||
|
||||
&.mx_SettingsTab_subsectionText .mx_SettingsFlag {
|
||||
margin-right: 0px !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,4 +22,25 @@ limitations under the License.
|
|||
.mx_SettingsTab_section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
||||
margin-right: 200px;
|
||||
|
||||
> div {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-18px;
|
||||
color: $primary-fg-color;
|
||||
margin: 16px 0;
|
||||
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ $spacePanelWidth: 71px;
|
|||
> p {
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_SpaceFeedbackPrompt {
|
||||
|
@ -51,13 +50,6 @@ $spacePanelWidth: 71px;
|
|||
}
|
||||
}
|
||||
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_BetaCard_betaPill {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.mx_SpaceCreateMenuType {
|
||||
@mixin SpacePillButton;
|
||||
}
|
||||
|
@ -100,6 +92,11 @@ $spacePanelWidth: 71px;
|
|||
width: min-content;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,9 @@ import RightPanelStore from "../../stores/RightPanelStore";
|
|||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { createSpaceFromCommunity } from "../../utils/space";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -399,6 +402,8 @@ class FeaturedUser extends React.Component {
|
|||
const GROUP_JOINPOLICY_OPEN = "open";
|
||||
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||
|
||||
const UPGRADE_NOTICE_LS_KEY = "mx_hide_community_upgrade_notice";
|
||||
|
||||
@replaceableComponent("structures.GroupView")
|
||||
export default class GroupView extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -422,6 +427,7 @@ export default class GroupView extends React.Component {
|
|||
publicityBusy: false,
|
||||
inviterProfile: null,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||
showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -807,6 +813,22 @@ export default class GroupView extends React.Component {
|
|||
showGroupAddRoomDialog(this.props.groupId);
|
||||
};
|
||||
|
||||
_dismissUpgradeNotice = () => {
|
||||
localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true");
|
||||
this.setState({ showUpgradeNotice: false });
|
||||
}
|
||||
|
||||
_onCreateSpaceClick = () => {
|
||||
createSpaceFromCommunity(this._matrixClient, this.props.groupId);
|
||||
};
|
||||
|
||||
_onAdminsLinkClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
});
|
||||
};
|
||||
|
||||
_getGroupSection() {
|
||||
const groupSettingsSectionClasses = classnames({
|
||||
"mx_GroupView_group": this.state.editing,
|
||||
|
@ -843,10 +865,46 @@ export default class GroupView extends React.Component {
|
|||
},
|
||||
) }
|
||||
</div> : <div />;
|
||||
|
||||
let communitiesUpgradeNotice;
|
||||
if (this.state.showUpgradeNotice) {
|
||||
let text;
|
||||
if (this.state.isUserPrivileged) {
|
||||
text = _t("You can create a Space from this community <a>here</a>.", {}, {
|
||||
a: sub => <AccessibleButton onClick={this._onCreateSpaceClick} kind="link">
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
});
|
||||
} else {
|
||||
text = _t("Ask the <a>admins</a> of this community to make it into a Space " +
|
||||
"and keep a look out for the invite.", {}, {
|
||||
a: sub => <AccessibleButton onClick={this._onAdminsLinkClick} kind="link">
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
communitiesUpgradeNotice = <div className="mx_GroupView_spaceUpgradePrompt">
|
||||
<h2>{ _t("Communities can now be made into Spaces") }</h2>
|
||||
<p>
|
||||
{ _t("Spaces are a new way to make a community, with new features coming.") }
|
||||
|
||||
{ text }
|
||||
|
||||
{ _t("Communities won't receive further updates.") }
|
||||
</p>
|
||||
<AccessibleButton
|
||||
className="mx_GroupView_spaceUpgradePrompt_close"
|
||||
onClick={this._dismissUpgradeNotice}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className={groupSettingsSectionClasses}>
|
||||
{ header }
|
||||
{ hostingSignup }
|
||||
{ changeDelayWarning }
|
||||
{ communitiesUpgradeNotice }
|
||||
{ this._getJoinableNode() }
|
||||
{ this._getLongDescriptionNode() }
|
||||
{ this._getRoomsNode() }
|
||||
|
|
|
@ -74,6 +74,10 @@ import { BetaPill } from "../views/beta/BetaCard";
|
|||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
|
||||
import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
|
||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -158,7 +162,33 @@ const onBetaClick = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
|
||||
// XXX: temporary community migration component
|
||||
const GroupTile = ({ groupId }: { groupId: string }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
|
||||
|
||||
if (!groupSummary) return <Spinner />;
|
||||
|
||||
return <>
|
||||
<GroupAvatar
|
||||
groupId={groupId}
|
||||
groupName={groupSummary.profile.name}
|
||||
groupAvatarUrl={groupSummary.profile.avatar_url}
|
||||
width={16}
|
||||
height={16}
|
||||
resizeMethod='crop'
|
||||
/>
|
||||
{ groupSummary.profile.name }
|
||||
</>;
|
||||
};
|
||||
|
||||
interface ISpacePreviewProps {
|
||||
space: Room;
|
||||
onJoinButtonClicked(): void;
|
||||
onRejectButtonClicked(): void;
|
||||
}
|
||||
|
||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
|
||||
|
@ -270,8 +300,18 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</div>;
|
||||
}
|
||||
|
||||
let migratedCommunitySection: JSX.Element;
|
||||
const createContent = space.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
|
||||
if (createContent[CreateEventField]) {
|
||||
migratedCommunitySection = <div className="mx_SpaceRoomView_preview_migratedCommunity">
|
||||
{ _t("Created from <Community />", {}, {
|
||||
Community: () => <GroupTile groupId={createContent[CreateEventField]} />,
|
||||
}) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_preview">
|
||||
<BetaPill onClick={onBetaClick} />
|
||||
{ migratedCommunitySection }
|
||||
{ inviterSection }
|
||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<h1 className="mx_SpaceRoomView_preview_name">
|
||||
|
|
|
@ -24,6 +24,8 @@ import { MenuItem } from "../../structures/ContextMenu";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
import { createSpaceFromCommunity } from "../../../utils/space";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
|
@ -49,6 +51,11 @@ export default class TagTileContextMenu extends React.Component {
|
|||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCreateSpaceClick = () => {
|
||||
createSpaceFromCommunity(this.context, this.props.tag);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveUp = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||
this.props.onFinished();
|
||||
|
@ -77,6 +84,16 @@ export default class TagTileContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let createSpaceOption;
|
||||
if (GroupStore.isUserPrivileged(this.props.tag)) {
|
||||
createSpaceOption = <>
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_createSpace" onClick={this._onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</MenuItem>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
|
@ -88,6 +105,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t("Unpin") }
|
||||
</MenuItem>
|
||||
{ createSpaceOption }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
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, { useEffect, useRef, useState } from "react";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import { GroupMember } from "../right_panel/UserInfo";
|
||||
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
groupId: string;
|
||||
onFinished(spaceId?: string): void;
|
||||
}
|
||||
|
||||
export const CreateEventField = "io.element.migrated_from_community";
|
||||
|
||||
interface IGroupRoom {
|
||||
displayname: string;
|
||||
name?: string;
|
||||
roomId: string;
|
||||
canonicalAlias?: string;
|
||||
avatarUrl?: string;
|
||||
topic?: string;
|
||||
numJoinedMembers?: number;
|
||||
worldReadable?: boolean;
|
||||
guestCanJoin?: boolean;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IGroupSummary {
|
||||
profile: {
|
||||
avatar_url?: string;
|
||||
is_openly_joinable?: boolean;
|
||||
is_public?: boolean;
|
||||
long_description: string;
|
||||
name: string;
|
||||
short_description: string;
|
||||
};
|
||||
rooms_section: {
|
||||
rooms: unknown[];
|
||||
categories: Record<string, unknown>;
|
||||
total_room_count_estimate: number;
|
||||
};
|
||||
user: {
|
||||
is_privileged: boolean;
|
||||
is_public: boolean;
|
||||
is_publicised: boolean;
|
||||
membership: string;
|
||||
};
|
||||
users_section: {
|
||||
users: unknown[];
|
||||
roles: Record<string, unknown>;
|
||||
total_user_count_estimate: number;
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [topic, setTopic] = useState("");
|
||||
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
|
||||
|
||||
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
|
||||
useEffect(() => {
|
||||
if (groupSummary) {
|
||||
setName(groupSummary.profile.name || "");
|
||||
setTopic(groupSummary.profile.short_description || "");
|
||||
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupSummary]);
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const onCreateSpaceClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
setBusy(false);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
setBusy(false);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
viaMap.set(roomId, calculateRoomVia(room));
|
||||
} else if (canonicalAlias) {
|
||||
try {
|
||||
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
|
||||
viaMap.set(roomId, servers);
|
||||
} catch (e) {
|
||||
console.warn("Failed to resolve alias during community migration", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!viaMap.get(roomId)?.length) {
|
||||
// XXX: lets guess the via, this might end up being incorrect.
|
||||
const str = canonicalAlias || roomId;
|
||||
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
|
||||
}
|
||||
}
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
[CreateEventField]: groupId,
|
||||
},
|
||||
initial_state: rooms.map(({ roomId }) => ({
|
||||
type: EventType.SpaceChild,
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
// don't bother awaiting this, as we don't hugely care if it fails
|
||||
cli.setGroupProfile(groupId, {
|
||||
...groupSummary.profile,
|
||||
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
|
||||
_t("This community has been upgraded into a Space") + `</h1></a><br />`
|
||||
+ groupSummary.profile.long_description,
|
||||
} as IGroupSummary["profile"]).catch(e => {
|
||||
console.warn("Failed to update community profile during migration", e);
|
||||
});
|
||||
|
||||
onFinished(roomId);
|
||||
|
||||
const onSpaceClick = () => {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const onPreferencesClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
};
|
||||
|
||||
let spacesDisabledCopy;
|
||||
if (!SpaceStore.spacesEnabled) {
|
||||
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
|
||||
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Space created"),
|
||||
description: <>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
|
||||
<p>
|
||||
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
|
||||
"been invited to it.", {}, {
|
||||
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
|
||||
{ name }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
|
||||
{ spacesDisabledCopy }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("To create a Space from another community, just pick the community in Preferences.") }
|
||||
</p>
|
||||
</>,
|
||||
button: _t("Preferences"),
|
||||
onFinished: (openPreferences: boolean) => {
|
||||
if (openPreferences) {
|
||||
onPreferencesClick();
|
||||
}
|
||||
},
|
||||
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let footer;
|
||||
if (error) {
|
||||
footer = <>
|
||||
<img src={require("../../../../res/img/element-icons/warning-badge.svg")} height="24" width="24" alt="" />
|
||||
|
||||
<span className="mx_CreateSpaceFromCommunityDialog_error">
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
|
||||
</span>
|
||||
|
||||
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
|
||||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Create Space from community")}
|
||||
className="mx_CreateSpaceFromCommunityDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||
<p>
|
||||
{ _t("A link to the Space will be put in your community description.") }
|
||||
|
||||
{ _t("All rooms will be added and all community members will be invited.") }
|
||||
</p>
|
||||
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
|
||||
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
|
||||
</p>
|
||||
|
||||
<SpaceCreateForm
|
||||
busy={busy}
|
||||
onSubmit={onCreateSpaceClick}
|
||||
avatarUrl={groupSummary.profile.avatar_url
|
||||
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
|
||||
: undefined
|
||||
}
|
||||
setAvatar={setAvatar}
|
||||
name={name}
|
||||
setName={setName}
|
||||
nameFieldRef={spaceNameField}
|
||||
topic={topic}
|
||||
setTopic={setTopic}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
showAliasField={joinRule === JoinRule.Public}
|
||||
aliasFieldRef={spaceAliasField}
|
||||
>
|
||||
<p>{ _t("This description will be shown to people when they view your space") }</p>
|
||||
<JoinRuleDropdown
|
||||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
/>
|
||||
<p>{ joinRule === JoinRule.Public
|
||||
? _t("Open space for anyone, best for communities")
|
||||
: _t("Invite only, best for yourself or teams")
|
||||
}</p>
|
||||
{ joinRule !== JoinRule.Public &&
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||
}
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default CreateSpaceFromCommunityDialog;
|
||||
|
|
@ -16,8 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
@ -27,8 +26,7 @@ import { BetaPill } from "../beta/BetaCard";
|
|||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import createRoom from "../../../createRoom";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
|
||||
|
@ -81,28 +79,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
|
|||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
createOpts: {
|
||||
preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
power_level_content_override: {
|
||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||
events_default: 100,
|
||||
...joinRule === JoinRule.Public ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: joinRule === JoinRule.Public && alias
|
||||
? alias.substr(1, alias.indexOf(":") - 1)
|
||||
: undefined,
|
||||
topic,
|
||||
},
|
||||
avatar,
|
||||
roomType: RoomType.Space,
|
||||
parentSpace,
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: true,
|
||||
inlineErrors: true,
|
||||
});
|
||||
await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace });
|
||||
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
UserTab.Preferences,
|
||||
_td("Preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab />,
|
||||
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
|
|
|
@ -851,7 +851,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
return <div />;
|
||||
};
|
||||
|
||||
interface GroupMember {
|
||||
export interface GroupMember {
|
||||
userId: string;
|
||||
displayname?: string; // XXX: GroupMember objects are inconsistent :((
|
||||
avatarUrl?: string;
|
||||
|
|
|
@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
|
@ -27,6 +29,18 @@ import SettingsFlag from '../../../elements/SettingsFlag';
|
|||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
||||
import GroupAvatar from "../../../avatars/GroupAvatar";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import GroupActions from "../../../../../actions/GroupActions";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||
import { CreateEventField, IGroupSummary } from "../../../dialogs/CreateSpaceFromCommunityDialog";
|
||||
import { createSpaceFromCommunity } from "../../../../../utils/space";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn(success: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
autoLaunch: boolean;
|
||||
|
@ -42,8 +56,86 @@ interface IState {
|
|||
readMarkerOutOfViewThresholdMs: string;
|
||||
}
|
||||
|
||||
type Community = IGroupSummary & {
|
||||
groupId: string;
|
||||
spaceId?: string;
|
||||
};
|
||||
|
||||
const CommunityMigrator = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [communities, setCommunities] = useState<Community[]>(null);
|
||||
useEffect(() => {
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(cli));
|
||||
}, [cli]);
|
||||
useDispatcher(dis, async payload => {
|
||||
if (payload.action === "GroupActions.fetchJoinedGroups.success") {
|
||||
const communities: Community[] = [];
|
||||
|
||||
const migratedSpaceMap = new Map(cli.getRooms().map(room => {
|
||||
const createContent = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
|
||||
if (createContent?.[CreateEventField]) {
|
||||
return [createContent[CreateEventField], room.roomId] as [string, string];
|
||||
}
|
||||
}).filter(Boolean));
|
||||
|
||||
for (const groupId of payload.result.groups) {
|
||||
const summary = await cli.getGroupSummary(groupId) as IGroupSummary;
|
||||
if (summary.user.is_privileged) {
|
||||
communities.push({
|
||||
...summary,
|
||||
groupId,
|
||||
spaceId: migratedSpaceMap.get(groupId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCommunities(communities);
|
||||
}
|
||||
});
|
||||
|
||||
if (!communities) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return <div className="mx_PreferencesUserSettingsTab_CommunityMigrator">
|
||||
{ communities.map(community => (
|
||||
<div key={community.groupId}>
|
||||
<GroupAvatar
|
||||
groupId={community.groupId}
|
||||
groupAvatarUrl={community.profile.avatar_url}
|
||||
groupName={community.profile.name}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{ community.profile.name }
|
||||
<AccessibleButton
|
||||
kind="primary_outline"
|
||||
onClick={() => {
|
||||
if (community.spaceId) {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: community.spaceId,
|
||||
});
|
||||
onFinished();
|
||||
} else {
|
||||
createSpaceFromCommunity(cli, community.groupId).then(([spaceId]) => {
|
||||
if (spaceId) {
|
||||
community.spaceId = spaceId;
|
||||
setCommunities([...communities]); // force component re-render
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ community.spaceId ? _t("Open Space") : _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
|
||||
export default class PreferencesUserSettingsTab extends React.Component<{}, IState> {
|
||||
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'breadcrumbs',
|
||||
];
|
||||
|
@ -52,6 +144,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
"Spaces.allRoomsInHome",
|
||||
];
|
||||
|
||||
static COMMUNITIES_SETTINGS = [
|
||||
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
|
||||
];
|
||||
|
||||
static KEYBINDINGS_SETTINGS = [
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
@ -242,6 +338,19 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
|
||||
</div> }
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
||||
<p>{ _t("Communities have been archived to make way for Spaces but you can convert your " +
|
||||
"communities into Spaces below. Converting will ensure your conversations get the latest " +
|
||||
"features.") }</p>
|
||||
<details>
|
||||
<summary>{ _t("Show my Communities") }</summary>
|
||||
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
||||
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
||||
</details>
|
||||
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
|
||||
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
|
||||
|
|
|
@ -18,22 +18,59 @@ import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, u
|
|||
import classNames from "classnames";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
||||
import createRoom from "../../../createRoom";
|
||||
import createRoom, { IOpts as ICreateOpts } from "../../../createRoom";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import withValidation from "../elements/Validation";
|
||||
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Modal from "../../../Modal";
|
||||
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
|
||||
export const createSpace = async (
|
||||
name: string,
|
||||
isPublic: boolean,
|
||||
alias?: string,
|
||||
topic?: string,
|
||||
avatar?: string | File,
|
||||
createOpts: Partial<ICreateRoomOpts> = {},
|
||||
otherOpts: Partial<Omit<ICreateOpts, "createOpts">> = {},
|
||||
) => {
|
||||
return createRoom({
|
||||
createOpts: {
|
||||
name,
|
||||
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
|
||||
power_level_content_override: {
|
||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||
events_default: 100,
|
||||
...isPublic ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: isPublic && alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||
topic,
|
||||
...createOpts,
|
||||
},
|
||||
avatar,
|
||||
roomType: RoomType.Space,
|
||||
historyVisibility: isPublic ? HistoryVisibility.WorldReadable : HistoryVisibility.Invited,
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: true,
|
||||
inlineErrors: true,
|
||||
...otherOpts,
|
||||
});
|
||||
};
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
|
@ -92,7 +129,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
type BProps = Pick<ComponentProps<typeof SpaceBasicSettings>, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
|
||||
type BProps = Omit<ComponentProps<typeof SpaceBasicSettings>, "nameDisabled" | "topicDisabled" | "avatarDisabled">;
|
||||
interface ISpaceCreateFormProps extends BProps {
|
||||
busy: boolean;
|
||||
alias: string;
|
||||
|
@ -106,6 +143,7 @@ interface ISpaceCreateFormProps extends BProps {
|
|||
export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||
busy,
|
||||
onSubmit,
|
||||
avatarUrl,
|
||||
setAvatar,
|
||||
name,
|
||||
setName,
|
||||
|
@ -122,7 +160,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
|||
const domain = cli.getDomain();
|
||||
|
||||
return <form className="mx_SpaceBasicSettings" onSubmit={onSubmit}>
|
||||
<SpaceAvatar setAvatar={setAvatar} avatarDisabled={busy} />
|
||||
<SpaceAvatar avatarUrl={avatarUrl} setAvatar={setAvatar} avatarDisabled={busy} />
|
||||
|
||||
<Field
|
||||
name="spaceName"
|
||||
|
@ -200,30 +238,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
createOpts: {
|
||||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
power_level_content_override: {
|
||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||
events_default: 100,
|
||||
...visibility === Visibility.Public ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: visibility === Visibility.Public && alias
|
||||
? alias.substr(1, alias.indexOf(":") - 1)
|
||||
: undefined,
|
||||
topic,
|
||||
},
|
||||
avatar,
|
||||
roomType: RoomType.Space,
|
||||
historyVisibility: visibility === Visibility.Public
|
||||
? HistoryVisibility.WorldReadable
|
||||
: HistoryVisibility.Invited,
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: true,
|
||||
inlineErrors: true,
|
||||
});
|
||||
await createSpace(name, visibility === Visibility.Public, alias, topic, avatar);
|
||||
|
||||
onFinished();
|
||||
} catch (e) {
|
||||
|
@ -233,10 +248,23 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
|
||||
let body;
|
||||
if (visibility === null) {
|
||||
const onCreateSpaceFromCommunityClick = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
||||
body = <React.Fragment>
|
||||
<h2>{ _t("Create a space") }</h2>
|
||||
<p>{ _t("Spaces are a new way to group rooms and people. " +
|
||||
"To join an existing space you'll need an invite.") }</p>
|
||||
<p>
|
||||
{ _t("Spaces are a new way to group rooms and people.") }
|
||||
|
||||
{ _t("What kind of Space do you want to create?") }
|
||||
|
||||
{ _t("You can change this later.") }
|
||||
</p>
|
||||
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Public")}
|
||||
|
@ -251,7 +279,15 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
onClick={() => setVisibility(Visibility.Private)}
|
||||
/>
|
||||
|
||||
<p>{ _t("You can change this later") }</p>
|
||||
<p>
|
||||
{ _t("You can also create a Space from a <a>community</a>.", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
<br />
|
||||
{ _t("To join an existing space you'll need an invite.") }
|
||||
</p>
|
||||
|
||||
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -1029,12 +1029,14 @@
|
|||
"e.g. my-space": "e.g. my-space",
|
||||
"Address": "Address",
|
||||
"Create a space": "Create a space",
|
||||
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.",
|
||||
"What kind of Space do you want to create?": "What kind of Space do you want to create?",
|
||||
"You can change this later.": "You can change this later.",
|
||||
"Public": "Public",
|
||||
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
|
||||
"Private": "Private",
|
||||
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
|
||||
"You can change this later": "You can change this later",
|
||||
"You can also create a Space from a <a>community</a>.": "You can also create a Space from a <a>community</a>.",
|
||||
"To join an existing space you'll need an invite.": "To join an existing space you'll need an invite.",
|
||||
"Go back": "Go back",
|
||||
"Your public space": "Your public space",
|
||||
"Your private space": "Your private space",
|
||||
|
@ -1337,12 +1339,18 @@
|
|||
"If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.",
|
||||
"Room ID or address of ban list": "Room ID or address of ban list",
|
||||
"Subscribe": "Subscribe",
|
||||
"Open Space": "Open Space",
|
||||
"Create Space": "Create Space",
|
||||
"Start automatically after system login": "Start automatically after system login",
|
||||
"Warn before quitting": "Warn before quitting",
|
||||
"Always show the window menu bar": "Always show the window menu bar",
|
||||
"Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close",
|
||||
"Preferences": "Preferences",
|
||||
"Room list": "Room list",
|
||||
"Communities": "Communities",
|
||||
"Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.",
|
||||
"Show my Communities": "Show my Communities",
|
||||
"If a community isn't shown you may not have permission to convert it.": "If a community isn't shown you may not have permission to convert it.",
|
||||
"Keyboard shortcuts": "Keyboard shortcuts",
|
||||
"To view all keyboard shortcuts, click here.": "To view all keyboard shortcuts, click here.",
|
||||
"Displaying time": "Displaying time",
|
||||
|
@ -2224,13 +2232,24 @@
|
|||
"Visible to space members": "Visible to space members",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
"Create Room": "Create Room",
|
||||
"This community has been upgraded into a Space": "This community has been upgraded into a Space",
|
||||
"To view Spaces, hide communities in <a>Preferences</a>": "To view Spaces, hide communities in <a>Preferences</a>",
|
||||
"Space created": "Space created",
|
||||
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
|
||||
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
|
||||
"Failed to migrate community": "Failed to migrate community",
|
||||
"Create Space from community": "Create Space from community",
|
||||
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
|
||||
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
|
||||
"Flair won't be available in Spaces for the foreseeable future.": "Flair won't be available in Spaces for the foreseeable future.",
|
||||
"This description will be shown to people when they view your space": "This description will be shown to people when they view your space",
|
||||
"Space visibility": "Space visibility",
|
||||
"Private space (invite only)": "Private space (invite only)",
|
||||
"Public space": "Public space",
|
||||
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
|
||||
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
|
||||
"Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
|
||||
"Add a space to a space you manage.": "Add a space to a space you manage.",
|
||||
"Space visibility": "Space visibility",
|
||||
"Private space (invite only)": "Private space (invite only)",
|
||||
"Public space": "Public space",
|
||||
"Want to add an existing space instead?": "Want to add an existing space instead?",
|
||||
"Adding...": "Adding...",
|
||||
"Sign out": "Sign out",
|
||||
|
@ -2682,7 +2701,6 @@
|
|||
"You must join the room to see its files": "You must join the room to see its files",
|
||||
"No files visible in this room": "No files visible in this room",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.",
|
||||
"Communities": "Communities",
|
||||
"Create community": "Create community",
|
||||
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n",
|
||||
"Add rooms to the community summary": "Add rooms to the community summary",
|
||||
|
@ -2710,6 +2728,11 @@
|
|||
"Community Settings": "Community Settings",
|
||||
"Want more than a community? <a>Get your own server</a>": "Want more than a community? <a>Get your own server</a>",
|
||||
"Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.",
|
||||
"You can create a Space from this community <a>here</a>.": "You can create a Space from this community <a>here</a>.",
|
||||
"Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.",
|
||||
"Communities can now be made into Spaces": "Communities can now be made into Spaces",
|
||||
"Spaces are a new way to make a community, with new features coming.": "Spaces are a new way to make a community, with new features coming.",
|
||||
"Communities won't receive further updates.": "Communities won't receive further updates.",
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
|
||||
"Featured Rooms:": "Featured Rooms:",
|
||||
"Featured Users:": "Featured Users:",
|
||||
|
@ -2841,6 +2864,7 @@
|
|||
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
|
||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>",
|
||||
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
|
||||
"Created from <Community />": "Created from <Community />",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import { Store } from 'flux/utils';
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import GroupStore from './GroupStore';
|
||||
import Analytics from '../Analytics';
|
||||
import * as RoomNotifs from "../RoomNotifs";
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { CreateEventField } from "../components/views/dialogs/CreateSpaceFromCommunityDialog";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
orderedTags: null,
|
||||
|
@ -235,8 +237,12 @@ class GroupFilterOrderStore extends Store {
|
|||
(t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t),
|
||||
);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const migratedCommunities = new Set(cli.getRooms().map(r => {
|
||||
return r.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[CreateEventField];
|
||||
}).filter(Boolean));
|
||||
const groupIdsToAdd = groupIds.filter(
|
||||
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId),
|
||||
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId) && !migratedCommunities.has(groupId),
|
||||
);
|
||||
|
||||
return tagsToKeep.concat(groupIdsToAdd);
|
||||
|
|
|
@ -20,11 +20,11 @@ import FlairStore from './FlairStore';
|
|||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
|
||||
function parseMembersResponse(response) {
|
||||
export function parseMembersResponse(response) {
|
||||
return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember));
|
||||
}
|
||||
|
||||
function parseRoomsResponse(response) {
|
||||
export function parseRoomsResponse(response) {
|
||||
return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom));
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { calculateRoomVia } from "./permalinks/Permalinks";
|
||||
import Modal from "../Modal";
|
||||
|
@ -37,6 +38,7 @@ import { leaveRoomBehaviour } from "./membership";
|
|||
import Spinner from "../components/views/elements/Spinner";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
|
||||
import CreateSpaceFromCommunityDialog from "../components/views/dialogs/CreateSpaceFromCommunityDialog";
|
||||
|
||||
export const shouldShowSpaceSettings = (space: Room) => {
|
||||
const userId = space.client.getUserId();
|
||||
|
@ -173,3 +175,10 @@ export const leaveSpace = (space: Room) => {
|
|||
},
|
||||
}, "mx_LeaveSpaceDialog_wrapper");
|
||||
};
|
||||
|
||||
export const createSpaceFromCommunity = (cli: MatrixClient, groupId: string): Promise<[string?]> => {
|
||||
return Modal.createTrackedDialog('Create Space', 'from community', CreateSpaceFromCommunityDialog, {
|
||||
matrixClient: cli,
|
||||
groupId,
|
||||
}, "mx_CreateSpaceFromCommunityDialog_wrapper").finished as Promise<[string?]>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue